集合的树型表示-脱线MIN问题

集合的树型表示-脱线MIN问题

问题描述:

        对于一个集合S,现在有两个操作,insert(i):将元素i插入到集合S中去,delete_min(i):从集合S中找出最小元素并进行删除。现给出一个insert和delete_min的指令队列,要求输出元素i是被第几条delete_min指令删除的。这就是脱线MIN问题。

       例如 7,2,5,9,-1,6,-1,-1,3,-1,1,4,-1,0(其中-1表示的是delete_min指令,0表示输入的结束,其他数字就表示是插入的数值)。结果为1(5)(1被第五条delete_min指令删除), 2(1), 3(4), 4(未被删除),5(2),6(3),7,9与4一样未被删除,8未出现。

       这种序列满足两个性质:

       1)  任一i (1≤i≤n) 在序列中最多出现一次(元素之间互不相同);

       2)从左起任意一段中,插入指令条数大于等于E指令条数,否则无元素可删。

问题解决:

分析:

       此问题基于集合的一些操作,现在规定如下的集合操作,FIND(i):找出元素i所在的集合名并返回,UNION(i,j,k)将集合名为i和j的集合合并为名为k的集合。

       算法开始之前,先把所有元素的所属集合名NAME[i]置为0(O(n));再扫描指令序列,把由E隔开的每段中的元素组成若干个集合并命名(O(n)):e.g.: 1={2,5,7,9},2={6},3= ,4={3},5={1,4},6= 用集合名(数字)来表示删除i的E指令序号。

       算法从i=1开始逐一检查,找到1所在的元素集合名(5),输出1是被第5条E指令删除的;输出后用UNION算法把集合5与其后的集合6合并为6:6={1,4}。

       下一步看i=2,找到2所在的元素集合名(1),输出2是被第1条E指令删除的;输出后用UNION算法把集合1与其后的2合并,得到2={2,5,6,7,9}。

        其次看i=3,找到3所在的元素集合名(4),输出3是被第4条E指令删除的;输出后用UNION算法把集合4与其后的集合6合并(此时集合5已经不存在了),得到6={1,3,4}。

        i=4时,找到4所在的元素集合名(6), 但6>E指令条数(只有5条),故输出“4未被删除”。

        i=5时,找到5所在的元素集合名(2),输出5是被第2条E指令删除的;输出后用UNION算法把集合2与其后的集合3合并,得3={2,5,6,7,9}。

        i=6时,找到6所在的元素集合名(3),输出6是被第3条E指令删除的;输出后用UNION算法把集合3与6合并,得6={1,2,3,4,5,6,7,9}

       其后的7,9执行Find后均得6,故与4一样未被删除,而8未在序列中出现,因Find(8)=0,故应输出“8未出现”。

       因此,依照以上的问题分析的过程,我们可以得出算法。

算法描述:

       引入Pred和Succ 2个数组:

       Pred[j]记录了前一个集合的名称(数字),初始时为j-1,

       Succ[j]记录了后一个集合的名称(数字),初始时为j+1。

       for i=1 to n do

         {    j←Find(i);  /*找到i所属集合名(数字)即删除i的delete_min指令序号*/

             if j=0 then { 输出“i未在序列中出现”}

             else if j>k then {输出“i未被删除”}

             else    /* i确实被删除了*/

                { 输出“i是被第j条delete_min指令所删除”;

                  UNION(j,Succ[j],Succ[j]);

                  Succ[Pred[j]]←Succ[j];/* 集合j不再存在*/

                  Pred[Succ[j]]←Pred[j]

                }

          }

C语言实现:

#include <stdio.h>
#include <stdlib.h>

#define		N		17

typedef  struct set_node{
	int name;
	int count;
	int father;
}set_node;

void init_set(set_node *set,int *root)
{
	int i;
	for(i=1;i<N;i++){
		(set+i)->name=0;
		(set+i)->count=0;
		(set+i)->father=0;
		root[i]=0;
	}
}

void set_setnode(set_node *set,int n)
{
	(set+n)->name=n;
	(set+n)->count=1;
	(set+n)->father=0;
}

int set_find(int num,set_node *set)
{
	if(set[num].name == 0) return 0;
	for(;set[num].father != 0; num=set[num].father);
	return set[num].name;
}

void set_insert(int num,int s,set_node *set,int *root)
{
	if(root[s] == 0){
		set[num].name=s;
		root[s]=num;
	}
	else{
		set[num].father=root[s];
		set[root[s]].count++;
	}
}

void set_union(int s1,int s2,int to,set_node *set,int *root)
{
	if(root[s1] == 0 && root[s2] == 0){
		root[to]=0;
	}
	else if(root[s1] == 0){
		set[root[s2]].name=to;
		root[to]=root[s2];
	}
	else if(root[s2] == 0){
		set[root[s1]].name=to;
		root[to]=root[s1];
	}
	else{
		int large,small;
		if(set[root[s1]].count >= set[root[s2]].count){
			large=root[s1];
			small=root[s2];
		}
		else{
			large=root[s2];
			small=root[s1];
		}

		set[small].father=large;
		set[large].count+=set[small].count;
		set[large].name=to;
		root[to]=large;
	}
}

int main()
{
	int i,j;
	int num,set_num,root[N];
	set_node set[N];
	
	init_set(set,root);set_num=1;
	while(1==scanf("%d",&num)){
		if(num == 0) break;
		if(num == -1){
			set_num++;
		}
		else{
			set_setnode(set,num);
			set_insert(num,set_num,set,root);
			//set_union(set_num,num,set_num,set,root);
		}
	}

	int pred[N],succ[N];
	for(i=1;i<N;i++)
	{
		pred[i]=i-1;
		succ[i]=i+1;
	}

	for(i=1;i<N;i++)
	{
		j=set_find(i,set);
		if(j == 0) printf("%d never appeared!\n",i);
		else if(j == set_num) printf("%d not been deleted!\n",i);
		else{
			printf("%d deleted by %d !\n",i,j);
			set_union(j,succ[j],succ[j],set,root);
			succ[pred[j]]=succ[j];
			pred[succ[j]]=pred[j];
		}
	}

	return 0;
}


 

运行结果截图:

反思:

       上面的算法执行的时候在UNION操作上得花费为O(1),就是说对于两个集合的合并,可以在常数时间里完成,但是我们来看FIND操作,执行一次FIND操作最坏的时候得需要O(logn)。对于本文中给出的问题还得需要O(n*logn)。现在反思的目的就是能否使得算法的时间复杂度下降点。这里,答案是肯定的。

       因为我们注意到:执行FIND[i]指令时, 必然会形成一条从结点i到根的路径P, 如果我们让该路径P上的所有非根节点均指向根(路径压缩),这样,下一次对该路径上的结点再执行FIND指令时,查找时间就会变短(因各结点已直接指向根节点)。

       对前述合并算法进行适当调整,新合并算法的思路如下:

假定集合名在1…n之间。用树的高度字段取代原Count字段(树中元素个数);合并集合时总是让原先树高值小的树的根指向树高值大的树的根。

       该算法引入4个数组:

       EXTERN_NAME[i]:表示名为i的集合对应的根节点号。

       INTERN_NAME[i]:表示根节点号i对应的集合名。

       P[i]:若i为根结点,则P[i]=i。若i不是根结点,则P[i]是i的父结点的编号。

       RANK[i](秩):在指令序列中删去FIND指令, 得到只含Union指令的新序列,RANK[i]是执行新序列(即未经压缩)时,以结点i为根的子树的树高。(若在执行中有Find指令,则会对树进行压缩,所以RANK[i]≥结点i实际的深度。)

改进的算法描述:

        FIND_PATH(i) /*找出i到根节点的路径,寻找的过程中顺便进行路径压缩*/

           if  i≠p[i]    /*i不是根*/    

              then  p[i]=Find(p[i]);

           return p[i];

 

        FIND(i)/*找出节点i所属的集合*/

         return INTERN_NAME[FIND_PATH(i)];

 

        Union(i,j,k)     /*i,j可以是任意结点,不一定是根结点*/

            a=EXTERN_NAME[i]          /*a是含结点i的树的根结点编号*/

            b=EXTERN_NAME[j]        /*b是含结点j的树的根结点编号*/

            if  a=b { EXTERN_NAME[k]=a;return;} /*i,j在同一个集合中,无需进行Union */

            if rank[a]>rank[b]  /*根结点为a的树‘深’,b指向a*/

               then {

                    P[b]=a;

                    EXTERN_NAME[k]=a

                    INTERN_NAME[a]=k  /*集合合并后重命名*/

               } else {

                   P[a]=b;

                   EXTERN_NAME[k]=b

                   INTERN_NAME[b]=k

                   if rank[a]=rank[b] then rank[b]增1; /*根结点为b的树‘深’或相等,a指向b*/

               }

改进后C语言的实现:

#include <stdio.h>
#include <stdlib.h>

#define		N	17

void init_array(int *p,int *rank,int *extern_name,int *intern_name)
{
	int i;
	for(i=0;i<N;i++)
	{
		*(p+i)=0;
		*(rank+i)=0;
		*(extern_name+i)=0;
		*(intern_name+i)=0;
	}
}

int set_find_path(int i,int *p)
{
	if(i != p[i])
		p[i]=set_find_path(p[i],p);
	return p[i];
}

int set_find(int i,int *p,int *intern_name)
{
	return intern_name[set_find_path(i,p)];
}

void set_union(int s1,int s2,int to,int *p,int *rank,int *intern_name,int *extern_name)
{
	int a,b;
	a=extern_name[s1];
	b=extern_name[s2];
	
	if(a==0 || b==0){
		if(a == 0) {
			extern_name[to]=b;
			intern_name[b]=to;
		}
		if(b == 0){
			extern_name[to]=a;
			intern_name[a]=to;
		}
		return;
	}

	if(a == b){
		extern_name[to]=a;
		return;
	}

	if(rank[a] > rank[b]){
		p[b]=a;
		extern_name[to]=a;
		intern_name[a]=to;
	}
	else{
		p[a]=b;
		extern_name[to]=b;
		intern_name[b]=to;
		if(rank[a] == rank[b]) rank[b]++;
	}

}

int main()
{
	int num,set_num,last_num;
	int p[N],rank[N],extern_name[N],intern_name[N];

	init_array(p,rank,extern_name,intern_name);
	last_num=0;set_num=1;
	while(1==scanf("%d",&num)){
		if(num == 0) break;
		if(num == -1){
			set_num++;
			last_num=0;
		}
		else{
			if(last_num == 0){
				extern_name[set_num]=num;
				intern_name[num]=set_num;
				p[num]=num;
			}
			else
				p[num]=last_num;
			last_num=num;
		}
	}

	int i,j;
	int pred[N],succ[N];
	for(i=1;i<N;i++)
	{
		pred[i]=i-1;
		succ[i]=i+1;
	}

	for(i=1;i<N;i++)
	{
		j=set_find(i,p,intern_name);
		if(j == 0) printf("%d never appeared!\n",i);
		else if(j == set_num) printf("%d not been deleted!\n",i);
		else{
			printf("%d deleted by %d !\n",i,j);
			set_union(j,succ[j],succ[j],p,rank,intern_name,extern_name);
			succ[pred[j]]=succ[j];
			pred[succ[j]]=pred[j];
		}
	}

	return 0;
}

 

改进后的运行结果与之前的运行结果一样,但是暂时还木有时间测试一下,按道理应该是O(n*G(n)),其中G(n)是阿克曼函数的逆函数,增长速度接近线性的,这个等有时间了再测试下吧O(∩_∩)O~~~that’s all~~~

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值