[ 编辑]1. 知识讲解
1.无论怎么样还是在合并的时候先判断是否属于同一集合,有些题合并两次会错。
2.一个节点的深度代表了该节点被移动合并的次数。
3.Rank代表的是子树可能的最大高度,一般可以不写。
4.带路径压缩的并查集合并和查询的时间复杂度略大于O(1)。
5.带权值的多用位运算,循环多用取模,s[]是偏移量。
6.表达式1,已知i和原root的关系及原root与现root的关系,求i与现root的关系
7.表达式2,已知i和原rooti的关系及j和原rootj的关系,求原rooti与原rootj的关系,其中一个已并到另外一个上
const int maxn=100;
int n;
int fa[maxn],s[maxn];
void init()//初始化,每个元素形成一个单独的集合
{
for (i=1;i<=n;i++)
fa[i]=i;
}
int find(int i)//查询i所在的集合
{
if (fa[i]!=i)
{
int tmp=fa[i];
fa[i]=find(fa[i]);
s[i]=//表达式 1
return fa[i];
}
return i;
}
bool Union(int i,int j)//将i所在的集合和j所在的集合合并
{
int fi=find(i),fj=find(j);
if (fi==fj)
return 0;
fa[fi]=fj;
s[fi]=//表达式 2
return 1;
}
[ 编辑]2. 例题分析
Poj 2492 A Bug’s Life
? 题目大意
虫分为雌性雄性,不同性别的虫才会有接触。给出一些接触虫子的编号,问是否存在同性恋。
? 题目分析
Define s[i]=0代表i与父节点同性别。
表达式1:tmp为保存下的i原来的父节点。表达式2
S[i]=0 S[i]=s[tmp] H S[i]=0/1 s[j]=0/1 S[fi]=s[fj]^1 S[i]=1 S[i]=s[tmp]^1 LL S[i]=0/1 S[j]=1/0 S[fi]=s[fj]
? 题目源码
#include<cstdio>
#include<cstring>
using namespace std;
int fa[2005];
bool s[2005];
int Find(int pos)
{
int tmpfa=fa[pos];//记录之前的父节点
if (fa[pos]==pos)
return pos;
fa[pos]=Find(fa[pos]);
if (!s[pos])//修改权值,第一个表达式
s[pos]=s[tmpfa];
else
s[pos]=!s[tmpfa];
return fa[pos];
}
bool Union(int i,int j)
{
int fi=Find(i),fj=Find(j);
if (fi==fj&&s[i]==s[j])//父节点相同,且与父节点的关系相同,同性恋
return false;
if (fi!=fj){
fa[fi]=fj;
s[fi]=!(s[i]^s[j]);//修改权值,第2个表达式
return true;
}
}
int main()
{
int a,b,t,n,m,i,ca=1;
bool f;
scanf("%d",&t);
while (t--){
f=true;
scanf("%d%d",&n,&m);
for (i=1;i<=n;i++)
fa[i]=i;
memset(s,false,sizeof(s));
for (i=0;i<m;i++){
scanf("%d%d",&a,&b);
if (f)
f=Union(a,b);
}
printf("Scenario #%d:\n%s bugs found!\n",ca++,f?"No suspicious":"Suspicious");
if (t!=0)
printf("\n");
}
return 0;
}2.5归并树
[ 编辑]●知识讲解
? 归并树利用类似线段树的树型结构记录合并排序的过程。 ? 以1 5 2 6 3 7为例: 把归并排序递归过程记录下来即是一棵归并树:
[1 2 3 5 6 7] [1 2 5] [3 6 7] [1 5] [2] [6 3] [7] [1][5] [6][3]
用对应的下标区间建线段树:(这里下标区间对应的是原数列)
[1 6] [1 3] [4 6] [1 2] [3] [4 5][6] [1][2] [4][5]
每次查找[l r]区间的第k大数时,在[1 2 3 4 5 6 7]这个有序的序列中二分所要找的数x,然后对应到线段树中去找[l r]中比x小的数有几个,即x的rank。由于线段树中任意区间对应到归并树中是有序的,所以在线段树中的某个区间查找比x小的数的个数也可以用二分在对应的归并树中找。这样一次查询的时间复杂度是log(n)^2。 要注意的是,多个x有相同的rank时,应该取最大的一个
1.建立归并树后我们得到了序列key[]的非降序排列,由于此时key[]内元素的rank是非递减的,因此key[]中属于指定区间[s,t]内的元素的rank也是非递减的,所以我们可以用二分法枚举key[]中的元素并求得它在[s,t]中的 rank值,直到该rank值和询问中的rank值相等; 2.那对于key[]中的某个元素val,如何求得它在指定区间[s,t]中的rank?这就要利用到刚建好的归并树:我们可以利用类似线段树的 query[s,t]操作找到所有属于[s,t]的子区间,然后累加val分别在这些子区间内的rank,得到的就是val在区间[s,t]中的 rank,注意到这和合并排序的合并过程一致; 3.由于属于子区间的元素的排序结果已经记录下来,所以val在子区间内的rank可以通过二分法得到。
? 复杂度分析:三次二分操作
1.第一次二分:从已经排好序的n个数中做二分,选出一个数tmp,并用后面两次二分求出其在所求区间内的位序。 2.第二次二分:用类似线段树的方法,选出区间[x,y]中所包含的归并树中的有序片段。 3.第三次二分:求出第二次二分中得到的各个片断中tmp的位序,总位序就是各个位序之和。 于是每次询问的复杂度是O(log n * log n * log n)
[ 编辑]●例题分析
poj2104
? 题目大意
给定一个序列a[1..n]和m个询问(s,t,rank)(1 <= n <= 100 000, 1 <= m <= 5 000),对于每个询问输出区间[s,t]中第rank小的值
? 题目分析
经典的k-th number问题,按以上的分析建立归并树解决问题。
? 题目源码
#include<cstdio> #include<cstring> #include<algorithm> using namespace std; const int maxn=100005; struct node{ int l,r; };// node tree[maxn*5]; int n,m,num[maxn],merg[20][maxn];//merg的最大深度为线段树的最大深度 struct MergeTree{ int from,to,no;//查询from到to中第no小的值 void creat(int l,int r,int k,int deep)//创建归并树,deep为深度 { tree[k].l=l,tree[k].r=r; if (l==r){ merg[deep][l]=num[l]; return ; } int mid=(l+r)>>1; creat(l,mid,2*k,deep+1);//递归创建 creat(mid+1,r,2*k+1,deep+1); int i=l,j=mid+1,p=l; while (i<=mid&&j<=r){//归并排序,记录下每个深度的排序序列 if (merg[deep+1][i]<merg[deep+1][j]) merg[deep][p++]=merg[deep+1][i++]; else merg[deep][p++]=merg[deep+1][j++]; } while (i<=mid) merg[deep][p++]=merg[deep+1][i++]; while (j<=r) merg[deep][p++]=merg[deep+1][j++]; } int query(int k,int key,int deep) { if (tree[k].l>to||tree[k].r<from) return 0; if (tree[k].l>=from&&tree[k].r<=to) return lower_bound(&merg[deep][tree[k].l],&merg[deep][tree[k].r]+1,key)-&merg[deep][tree[k].l]; // lower_bound返回不小于key的第一个位置,利用的二分查找.可自己写二分函数 return query(2*k,key,deep+1)+query(2*k+1,key,deep+1);// } int slove()//二分枚举所有的数,logn { no--; int l=1,r=n,mid,ans; while (l<r){ mid=(l+r+1)>>1; ans=query(1,merg[1][mid],1);// if (ans<=no) l=mid; else r=mid-1; } return merg[1][l]; } }MergeT; int main() { while (scanf("%d%d",&n,&m)!=EOF){ for (int i=1;i<=n;i++) scanf("%d",num+i); MergeT.creat(1,n,1,1);// while (m--){ scanf("%d%d%d",&MergeT.from,&MergeT.to,&MergeT.no);// printf("%d\n",MergeT.slove()); //查询原序列中第MergeT.from个元素到MergeT.to个元素中第MergeT.no大的是多少 } } return 0; }
霍夫曼树
[ 编辑]●知识精讲
霍夫曼树,也称最优树,是一种带权外路径长度最短的树。 相关概念为:
路径长度是指树中两个结点间路径上所含有的分支数目。 树的路径长度是指从树根到每一结点的路径长度之和。 带权路径长度是指结点的路径长度与该结点的权之积。 树的带权外路径长度是树中所有叶子结点的带权路径长度之和。
设各叶子结点的路径长度为 lk,权为wk ,则该树的带权外路径长度为:
WPL = ∑ wk|lk 。
则 WPL 最小的二叉树即为最优二叉树或霍夫曼树。
霍夫曼树中除叶子结点外,所有分支结点的度均为 2,叶子结点(外部结点)可看成是由分支结点(内部结点)组成的树扩充出来的,因此,霍夫曼树是一棵扩充二叉树。
霍夫曼树的构造:
1、把给定的 n 个权值集合的各结点均作为一棵树; 2、从这 n 棵树中选取两个权值最小的结点组成新树; 3、将这两个结点的权值之和作为新树树根的权值; 4、将这两个结点从 n 棵树中删去,把新树树根加入; 5、原来的 n 棵树减少为 n-1 棵树; 6、重复步骤 2 -- 5,直到 n=1 即为所求。
例如:已知字符 A、B、C、D、E、F、G,使用频度分别为:0.25、0.1、0.15、0.06 、 0.3 、 0.05、0.09,试以此构造霍夫曼树。
霍夫曼树的应用:
1、最佳判定树; 2、霍夫曼编码。
霍夫曼树,即最优二叉树,带权路径长度最小的二叉树,经常应用于数据压缩,霍夫曼编码就是基于霍夫曼树的一种压缩编码。霍夫曼编码是可变字长编码(VLC)的一种,完全依据字符出现概率来构造平均长度最短的码字。
霍夫曼编码:
WPL霍夫曼编码 = 2.56
等长编码:
WPL等长编码 = 3 利用霍夫曼编码可提高效率:( 3 - 2.56 ) / 3 ≈ 15%