集合的树型表示-脱线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~~~