并查集
作用:
1.将两个集合合并
2.询问两个元素是否在一个集合当中
时间复杂度近乎O(1)
每个集合用一棵树表示,树根的编号就是集合的编号
每个节点存储他的父节点,p[x]表示x的父节点
Q:
1.如何判断树根: p[x]=x
2.如何求x的集合编号 while(p[x]!=x) x=p[x]
3.如何合并两个集合 : px是x的集合编号,py是y的集合编号。p[x]=y
优化:路径压缩
注意find
if(p[x)!=x)
这里是if不是while
#include<iostream>
using namespace std;
const int N=1e6;
int p[N];
int find(int x){//查找x的根节点(集合编号)
if(p[x]!=x)//如果不是根节点
p[x]=find(p[x]);//查找的同时也进行了路径压缩优化
return p[x];
}
void merge(int a,int b){//合并操作
p[find(b)]=find(a);//注意这里两个都是find()
}
string s;
int a,b;
int n,m;
int main(){
cin>>n>>m;
for(int i=1;i<=n;i++)
p[i]=i;//因为刚开始每个元素分别属于一个集合,因此需要初始化
while(m--){
cin>>s>>a>>b;
if(s=="M"){
merge(a,b);
}
if(s=="Q"){
if(find(a)==find(b))
cout<<"Yes"<<endl;
else
cout<<"No"<<endl;
}
}
return 0;
}
查找某个集合中元素个数
注意先更新个数再merge
如果先merge,那么更新的元素个数就是合并之后
#include<iostream>
using namespace std;
const int N=1e6;
int p[N];
int find(int x){//查找x的根节点(集合编号)
if(p[x]!=x)//如果不是根节点
p[x]=find(p[x]);//查找的同时也进行了路径压缩优化
return p[x];
}
void merge(int a,int b){//合并操作
p[find(b)]=find(a);//注意这里两个都是find()
}
string s;
int a,b;
int n,m;
int si[N];
int main(){
cin>>n>>m;
for(int i=1;i<=n;i++){
p[i]=i;//因为刚开始每个元素分别属于一个集合,因此需要初始化
si[i]=1;
}
while(m--){
cin>>s;
if(s=="C"){
cin>>a>>b;
if(find(a)!=find(b)){
si[find(a)]+=si[find(b)];//注意这里要先更新总数再做合并操作
merge(a,b);
}
}
if(s=="Q1"){
cin>>a>>b;
if(find(a)==find(b))
cout<<"Yes"<<endl;
else
cout<<"No"<<endl;
}
if(s=="Q2"){
cin>>a;
cout<<si[find(a)]<<endl;
}
}
return 0;
}
食物链
食物链
真的好难
#include<iostream>
using namespace std;
int n,k;
const int N=5*1e5;
const int M=3*1e6;
int p[N];
int d[N];
int find(int x){
if(p[x]!=x) {
/*
int t=p[x];//先存一下该点,因为下一步被更新
p[x]=find(p[x]);//路径压缩(表明该点已知)
d[x]+=d[t];
*/
int t = find(p[x]);
//利用find()得到x所在的根节点是t
//但是此时p[x]并没有更新,不过在执行find的过程中,从x到跟的路径上的所有点都更新了p[]和d[]
//因此下面语句中的d[p[x]]是p[x]到根节点的距离,就是x的父节点到根节点的距离
//如果x的父节点就是根节点,即p[x]=x,那么d[x]+=0,d[x]仍是原值,表示其到父节点的距离,也是根节点距离
d[x] += d[p[x]];
//d[i]的含义是x到其父节点的距离,并不一定是根节点,因为不一定更新到根节点了(或者根节点又加入了另一个树中,就不是根节点了)
//那么d[i]到
p[x] = t;//更新x的根节点,路径上的那些都已经更新过了
}
return p[x];
}
int main(){
cin>>n>>k;
for(int i=1;i<=n;i++){
p[i]=i;
d[i]=0;//其实也可以省略,全局数组本来就是0
}
int q,x,y;
int ans=0;
while(k--){
cin>>q>>x>>y;
int px=find(x);int py=find(y);
//找到各自根节点
//而且因为使用了find(),所以当前的d[x],d[y]都是到根节点的距离
//而p[x]p[y]都是根节点的值
if(x>n||y>n) ans++;
else{
if(q==1){
//表明x,y是同类
if(px==py&&(d[x]-d[y])%3!=0) ans++;//如果已经在一个集合中但已经知道不是一个类,因此这个是假话
//注意第二个判断不写d[x] % 3 != d[y] % 3,因为d[i]可能是负数
else if(px!=py){
//说明是真话,那么就维护
p[px]=py;//将x,y维护到同一个集合中
d[px]=d[y]-d[x];
//这个看截图
}
}
else{
if(x==y) ans++;
else if(px==py){
//根据我们的定义%=1吃%=0,%=2吃%=1,%=0吃%=2
//x吃y
//0-2-1模3也可以判断
if((d[x]-d[y]-1)%3){
//如果不是满足的情况
ans++;
}
}
else if(px!=py){
//x吃y
p[px]=py;
d[px]=d[y]-d[x]+1;
}
}
}
}
cout<<ans;
return 0;
}
字符串归类
字符串归类
写了一个超时的算法,过了11/20
/*
给定 n 个由小写字母构成的字符串。
现在,请你对它们进行归类。
对于两个字符串 a 和 b:
如果至少存在一个字母在 a 和 b 中同时出现,则 a 和 b 属于同一类字符串。
如果字符串 c 既与字符串 a 同类,又与字符串 b 同类,则 a 和 b 属于同一类字符串。
请问,最终所有字符串被划分为多少类。
输入格式
第一行包含整数 n。
接下来 n 行,每行包含一个仅由小写字母构成的字符串。
注意,输入字符串可能相同。
输出格式
一个整数,表示最终所有字符串被划分为的类的数量。
数据范围
前 6 个测试点满足 1≤n≤10。
所有测试点满足 1≤n≤2×105,输入字符串的长度范围 [1,50],所有输入字符串的总长度范围 [1,106],所有字符串均由小写英文字母构成。
输入样例1:
4
a
b
ab
d
输出样例1:
2
输入样例2:
3
ab
bc
abc
输出样例2:
1
输入样例3:
1
abcdefghijklmn
输出样例3:
1
*/
//一般一秒对应4*10^8次计算
#include<iostream>
#include<set>
using namespace std;
const int N=3*1e5;
int judge(string x,string y){
set <char> a;
set <char> b;
set <char> c;
for(auto m:x)
a.insert(m);
for(auto m:y)
b.insert(m);
for(auto m:x)
c.insert(m);
for(auto m:y)
c.insert(m);
if(c.size()<a.size()+b.size())
return 1;
return 0;
}
int p[N];
int find(int x){
if(p[x]!=x) p[x]=find(p[x]);
return p[x];
}
string a[N];
int n;
int main(){
cin>>n;
for(int i=1;i<=n;i++){
cin>>a[i];
p[i]=i;
}
for(int i=1;i<n;i++){
for(int j=i+1;j<=n;j++){
if(judge(a[i],a[j])==1){
//如果某两个字符串同类,查找是否其中已经有归类的
int pi=find(i),pj=find(j);
if(pi==pj)
continue;
else if(pi!=i||pj!=j){
p[pi]=pj;
}
else
p[i]=find(j);
}
}
}
int ans=0;
for(int i=1;i<=n;i++)
if(p[i]==i)
ans++;
cout<<ans;
return 0;
}
看了题解后发现,并不需要对于每个字符串处理。
其实既然具有相同字母的字符串是一类,并且和同一个字符串是一类的两个字符串是一类。那么实际上最多就26类(字符串分别是26个互不相同的字母时),因此可以对字符串的元素进行集合合并
合并规则:
所有含有字母a的为一个集合,含有字母b的为一个集合,以此类推。这就满足了如果至少存在一个字母在 a 和 b 中同时出现,则 a 和 b 属于同一类字符串这一条。
然后对于如果字符串 c 既与字符串 a 同类,又与字符串 b 同类,则 a 和 b 属于同一类字符串这一条,翻译过来就是,如果某个字符串既含有a,又含有b,那么ab为一类。
值得一提的是,对于某个字符串,并不需要执行时间复杂度为O(n方)的两两判别,只需要选定第一个字符,然后直接O(n)就可以,因为一个字符串中所有的字符都会维护到同一个集合中。
#include<iostream>
using namespace std;
const int N=27;
int p[N];
int j[N];//判断元素是否用过
int n;
string s;
int find(int x){
if(p[x]!=x) p[x]=find(p[x]);
return p[x];
}
int main(){
cin>>n;
for(int i=1;i<=26;i++)
p[i]=i;
while(n--){
cin>>s;
//记录第一个
j[s[0]-'a'+1]=1;//表明这个元素出现过
p[s[0]-'a'+1]=find(s[0]-'a'+1);
int x=find(s[0]-'a'+1);//第一个字符所在集合为x
//如果某个字符串第一个元素是在已有的集合中,那么后面所有元素都归入这个集合了
for(int i=1;i<s.size();i++){
int m=s[i]-'a'+1;//字母编号
j[m]=1;
int pm=find(m);//找到其根
if(pm!=x){//如果当前两个集合未合并
p[pm]=x;
}
}
}
int ans=0;
for(int i=1;i<=26;i++){
if(p[i]==i&&j[i])
ans++;
}
cout<<ans;
return 0;
}