图论系列:
前言:
狼煙を上げろ
猫はきっと戻らぬ
格子を開けろ
降り積る雪雪
相关题单:戳我
算法讲解:戳我
本题单部分题要求掌握各种STL的用法以及某些经典树上问题求法&算法。
CF1242B 0-1 MST
一类经典的依靠图的性质做的题。有一张完全图,\(n\) 个节点,有 \(m\) 条边的边权为 \(1\),其余的都为 \(0\),将边权为 \(1\) 的 \(m\) 条边告诉你,询问问你这张图的最小生成树的权值。 \(n,m \leq 1e5\)。
那么对于一张完全图,每两个点之间都会有一条边,那么边数是 \(n*(n-1)/2\) ,显然没办法将所有的边建出来跑最小生成树,这个时候就需要用到图的性质。
由于是完全图,换一个问法,如果所有点都连向一个点,那么连向那个点最优?自然是拥有给定边权 1 的出边最少的那个点(这样其他点都连向这个点,边权 \(1\) 的边选的最少),于是我们找出这样的一个点,那么所有与它相连边权是 0 的点我们就直接全部连上,对于那些与选定点连起来边权为 \(1\) 的点,它们可能与那些与选定点已经相连的点有边权为 \(0\) 的边。
于是在这些剩余的点,每个点依次遍历 \(n\) 个点,如果之间的边权是 0,就将这两个点合并起来。做完之后相当于只用边权为 \(0\) 的边将原图的连通块减到最少,那么现在答案就是连通块的数量 \(-1\),因为连通块之间必然只能用边权为 \(1\) 的边来连通了。
为什么这样做的时间复杂度是对的,因为实际上在 \(n=1e5\) 的时候,边权为 \(1\) 的点连起来的图肯定是一个稀疏图,此时点度数最少的那个点最多与 \(2m/n\) 个边相连(\(m\) 条边贡献 \(2m\) 的度数嘛,每个点平均分配),于是会有 \(2m/n\) 个点暴力遍历 \(n\) 个点查找是否边权为 0,所以时间复杂度就是 \(O(2m/n*n)=O(m)\) 的,总体时间复杂度为 \(O(n+m)\),自然随便跑。
代码:
const int M=5e5+5;
int n,m,minn=1,tot=0;
int fa[M],vis[M],in[M],t[M];
vector<int> e[M];inline int find(int x)
{if(x!=fa[x]) fa[x]=find(fa[x]);return fa[x];
}
inline void merge(int x,int y)
{x=find(x),y=find(y);if(x==y) return ;fa[x]=y;
}signed main()
{ios::sync_with_stdio(false);cin.tie(0);cout.tie(0);cin>>n>>m;for(int i=1;i<=n;++i) fa[i]=i;for(int i=1,a,b;i<=m;++i){cin>>a>>b,++in[a],++in[b];e[a].push_back(b),e[b].push_back(a);}for(int i=1;i<=n;++i) if(in[minn]>in[i]) minn=i;//找出拥有最少 1 出边的点for(auto v:e[minn]) vis[v]=1;//标记为1 的这些点就是和选定点之间的边的边权为 1for(int i=1;i<=n;++i) if(!vis[i]) merge(i,minn);//把边权为 0 的合并在一起for(int i=1;i<=n;++i){if(!vis[i]) continue;//暴力跑n个点的只是与选定点之间边权为1的点for(auto v:e[i]) t[v]=i;t[i]=i;//标记一下当前点与哪些点相连的边权为1,那么与剩下的点之间边权肯定为0,将这个点与剩下的点都合并起来for(int j=1;j<=n;j++){if(t[j]==i) continue;merge(i,j);}}for(int i=1;i<=n;++i){if(find(i)==i) ++tot;}//最后统计连通块的数量cout<<tot-1<<"\n";return 0;
}
CF1927F Microcycle
题目中给定的环的权值是在环上所有边的边权最小值(所以不是经典的求最小环中环的权值是边权值和了)。所以我们考虑环什么时候才会出现,自然是在一颗树上再添加一条边。
生成一颗树?自然可以用最小生成树,然后如果当前边的两点已经连通的话肯定加入这条边就会出现一个环了。但是用最小生成树有一个缺点,我们没法确定当前这个环的权值(其实你可以把最小生成树建出来,树剖后枚举多余的边,查询路径上边权最小的点)。
由于环的权值是边权最小的那条边,转化下思路,我们可以按边权从大到小的加入每一条边,这样当前边连接的两点已经连通,此时会生成一个环,环的权值就一定是当前边的权值(因为之前的边权都大于等于这条边),那么最后一条这样的边,其所在环的权值一定是最小的。那么将这颗生成树建出来,然后从这条边连接的两点中一点出发,dfs
到另一点,路上经过的点就是这个环上的所有点。
代码:
int T,n,m,flag,last,tot;
int fa[M],ans[M];struct node{int u,v,w;bool operator <(const node &o) const{return o.w<w;}//按边权从大到小
};node e[M];int cnt=0;
struct N{int to,next;
};N p[M<<1];
int head[M];inline void add(int a,int b)
{++cnt;p[cnt].next=head[a];head[a]=cnt;p[cnt].to=b;
}inline int find(int x)
{if(x!=fa[x]) fa[x]=find(fa[x]);return fa[x];
}inline void dfs(int u,int f)
{if(u==e[last].v) {flag=1,ans[++tot]=u;return ;}for(int i=head[u];i!=0;i=p[i].next){int v=p[i].to;if(v==f) continue;dfs(v,u);if(flag) {ans[++tot]=u;return ;}}
}signed main()
{ios::sync_with_stdio(false);cin.tie(0);cout.tie(0);cin>>T;while(T--){cin>>n>>m,last=cnt=flag=tot=0;for(int i=1;i<=n;++i) fa[i]=i,head[i]=0;for(int i=1;i<=m;++i) cin>>e[i].u>>e[i].v>>e[i].w;sort(e+1,e+m+1);for(int i=1,fx,fy;i<=m;++i){fx=find(e[i].u),fy=find(e[i].v);if(fx==fy) {last=i;continue;}//找对最后一个可以成环的边,此时其所在环的权值一定最小fa[fx]=fy,add(e[i].u,e[i].v),add(e[i].v,e[i].u);//将最大生成树建出来}dfs(e[last].u,0);//dfs出路径cout<<e[last].w<<" "<<tot<<"\n";//环的权值就是最后那条边的权值for(int i=1;i<=tot;++i) cout<<ans[i]<<" ";cout<<"\n";}return 0;
}
CF1012B Chemical table
模拟题,题意是在一个大矩形上,对于一个子矩阵,如果这个矩阵的 3 个顶点都被标记,那么第 4 个顶点会被自动标记,问你至少需要手动标记几个格子,使得整个矩形内的格子都被标记。
将每列与每边抽象成一个点,那么图上标记的点 \((x,y)\) 相当于是将第 \(x\) 行和第 \(y\) 列合并在一起。最后的答案就是将已经给定标记的点的 \(x\) 与 \(y\) 合并起来,然后看图中还有多少连通块。
洛谷上有一道双倍经验:P5089
代码:
int n,m,q,ans;
int fa[M<<1];inline int find(int x)
{if(x!=fa[x]) fa[x]=find(fa[x]);return fa[x];
}
signed main()
{ios::sync_with_stdio(false);cin.tie(0);cout.tie(0);cin>>n>>m>>q;for(int i=1;i<=n+m;i++) fa[i]=i;for(int i=1,x,y;i<=q;i++){cin>>x>>y;x=find(x),y=find(y+n);if(x==y) continue;fa[x]=y;}for(int i=1;i<=n+m;i++) if(find(i)==i) ++ans;cout<<ans-1<<"\n";return 0;
}
CF437D The Child and Zoo
和 CF1213G Path Queries 比较类似,这题是点有点权。记某一条路径的权值是经过点的最小权值,那么我们先点权转边权,一条边的边权就是其所连接的两个点的点权的较小值,路径权值同时也是经过的边的边权最小值。
需要找出任意两个点 \(x,y\) 之间所有可能的路径,\(f(x,y)\) 就等于这些路径中权值最大的那条。那么还是考虑按边权从大到小的加入边,如果加入一条边使得两点连通,那么这两点之间的 \(f\) 值就是加入的这条边的边权(够经典了吧)。
那么对于加入的这条边会对多少个点对产生影响?考虑对于边 \(x \to y\) , \(x\) 所在的连通块大小为 \(siz_x\) , \(y\) 所在的连通块大小为 \(siz_y\) ,原本 \(x\) 连通块内的点与 \(y\) 连通块内的点都不连通,但是加入这条边之后就连通了,于是有 \(siz_x*siz_y\) 个点对(从 \(x\) 中选一个,从 \(y\) 中选一个,乘法原理)的 \(f\) 值就是这条边的边权 \(w\)。我们用 \(ans\) 存下所有点对的 \(f\) 值,最后除以 \(n*(n-1)\),那么这条边产生的贡献就是 \(siz_x*siz_y*w\)。
代码:
const int M=1e5+5;
int n,m;
int c[M],fa[M],siz[M];
double ans=0;struct node{int u,v,w;inline bool operator < (const node &o) const{return w>o.w;}
};node a[M];inline int find(int x)
{if(x!=fa[x]) fa[x]=find(fa[x]);return fa[x];
}
signed main()
{cin>>n>>m;for(int i=1;i<=n;++i) cin>>c[i],siz[i]=1,fa[i]=i;for(int i=1;i<=m;++i){cin>>a[i].u>>a[i].v,a[i].w=min(c[a[i].u],c[a[i].v]);//边权转点权}sort(a+1,a+m+1);//从大到小的加边for(int i=1,x,y;i<=m;++i){x=find(a[i].u),y=find(a[i].v);if(x==y) continue;ans+=siz[x]*siz[y]*a[i].w;siz[y]+=siz[x],fa[x]=y;}printf("%.6lf\n",(double)(ans/(n*(n-1)/2)));return 0;
}
CF1133F2 Spanning Tree with One Fixed Degree
首先如果一开始图中 1 的度数就小于 \(D\),自然无解。由于最后生成的是颗树,所有点是连通的,那么考虑将 1 从原图中删去,判断一下现在图中有几个连通块,对于每一个连通块,由于最后要连通,所以 1 至少需要向每个连通块连一条边,所以连通块的数量大于 \(D\) ,也是无解的。
剩下的情况肯定都能构造出一颗合法的树,1 先向每个连通块连一条边,然后 \(D\) 还有余就再选几条边,最后保证生成的是颗树即可。
代码:
const int M=2e5+5;
int n,m,k,res;
int fa[M],vis[M];
vector<int> t;int cnt=0;
struct N{int to,next;
};N p[M<<2];
int head[M];
inline void add(int a,int b)
{++cnt;p[cnt].next=head[a];head[a]=cnt;p[cnt].to=b;
}inline int find(int x)
{if(x!=fa[x]) fa[x]=find(fa[x]);return fa[x];
}
inline void merge(int x,int y)
{x=find(x),y=find(y);if(x==y) return ;fa[x]=y;
}inline void bfs()
{queue<int> q;q.push(1),vis[1]=1;while(q.size()){int u=q.front();q.pop();for(int i=head[u];i!=0;i=p[i].next){int v=p[i].to;if(!vis[v]) vis[v]=1,q.push(v),cout<<u<<" "<<v<<"\n"; }}
}signed main()
{ios::sync_with_stdio(false);cin.tie(0);cout.tie(0);cin>>n>>m>>k;for(int i=1;i<=n;++i) fa[i]=i;for(int i=1,a,b;i<=m;++i){cin>>a>>b;if(a>b) swap(a,b);if(a!=1) merge(a,b),add(a,b),add(b,a);else t.push_back(b);}for(int i=2;i<=n;++i) res+=(find(i)==i);if(res>k||t.size()<k) {cout<<"NO\n";return 0;}cout<<"YES\n";for(auto &it:t){if(vis[find(it)]) continue;vis[find(it)]=1;add(1,it),add(it,1),it=-1,--k;}//像每个连通块连边if(k){for(auto it:t){if(it==-1) continue;add(1,it),add(it,1),--k;if(!k) break;}//还有余的话继续连}for(int i=1;i<=n;++i) vis[i]=0;bfs();return 0;
}
CF566D Restructuring Company
数据结构维护并查集,结果稍微优化一下即可。一开始每个点都是一个集合,有三种操作:
-
合并 \(x\) 所在的集合与 \(y\) 所在的集合。
-
合并 \(x,x+1...y\) 所在的集合。
-
查询 \(x\) 与 \(y\) 是否在同一个集合。
由于有第二种操作,显然没法直接暴力合并。但考虑到题目中只有合并操作,也就是说在同一个集合中的点一定一直都在同一个集合中,所以对于每一个点维护一个 \(pre\) 数组,\(pre_i\) 表示 \(i\) 点下一个不与其在同一个集合的点(有点像链表了),于是对于操作二,每一次只需要不断的跳 \(pre_i\) 直到跳到 \(y\)。
代码:
const int M=2e5+5;
int n,q;
int fa[M],pre[M];inline int find(int x)
{if(x!=fa[x]) fa[x]=find(fa[x]);return fa[x];
}inline int merge(int x,int y)
{x=find(x),y=find(y);if(x==y) return 0;fa[x]=y;return 1;
}signed main()
{cin>>n>>q;for(int i=1;i<=n;++i) fa[i]=i,pre[i]=i+1;int opt,x,y;while(q--){cin>>opt>>x>>y;if(opt==1) merge(x,y);else if(opt==2){for(int i=x+1,v;i<=y;i=v){merge(i-1,i);v=pre[i],pre[i]=pre[y];//由于 x-y 的所有集合都会被合并,那么中间这些点下一个不与其在一个集合的点自然就是y下一个不与其在一个集合的点}pre[x]=y+1;}else{if(find(x)==find(y)) cout<<"YES"<<endl;else cout<<"NO"<<endl;}}return 0;
}
CF228E The Road to Berland is Paved With Good Intentions
非常好的一道扩展域并查集,考虑怎么转化。因为每条边边权为 \(0/1\),每个点操作一次可以使得与这个点相连的边的边权异或上 \(1\),需要让最后所有边的边权都变为 \(1\),并输出一种操作方案。因为边权为 \(0/1\) ,操作只会让 \(0\) 变成 \(1\) ,\(1\) 变成 \(0\) ,所以一个点必然只会操作一次或者不操作(操作两次相当于不操作)。
那么考虑对于一条边权为 0/1 的边对于其连接的两点有什么限制?如果边权为 \(1\) ,那么说明此时连接的两点要么操作数都为 \(0\) ,要么操作数都为 \(1\) ,所以限制是连接的两点操作数相同(才能抵消操作的影响,因为边权已经是 \(1\) 了);那么对于边权为 \(0\) 的边,限制自然就是连接的两点操作数不同。
于是我们就可以使用扩展域并查集,\(i\) 连接于自己操作数相同的点,\(i+n\) 连接与自己操作数不同的点,根据每条边的限制进行连边即可,无解的情况就是出现的限制出现了矛盾。
输出方案也简单,因为最后只分成了两组嘛,其中一组的点操作 1 次即可。
代码:
const int M=105;
int n,m;
int fa[M<<1],vis[M<<1];
vector<int> ans;inline int find(int x)
{if(x!=fa[x]) fa[x]=find(fa[x]);return fa[x];
}
inline void merge(int x,int y)
{x=find(x),y=find(y);if(x==y) return ;fa[x]=y;
}
signed main()
{ios::sync_with_stdio(false);cin.tie(0);cout.tie(0);cin>>n>>m;for(int i=1;i<=n*2;++i) fa[i]=i;for(int i=1,opt,x,y;i<=m;++i){cin>>x>>y>>opt;if(opt==0){if(find(x)==find(y)) {cout<<"Impossible\n";return 0;}//矛盾了merge(x,y+n),merge(x+n,y);//操作数不相同}else{if(find(x)==find(y+n)||find(y)==find(x+n)) {cout<<"Impossible\n";return 0;}merge(x,y),merge(x+n,y+n);//操作数相同}}for(int i=1;i<=n*2;i++) fa[i]=find(i);for(int i=1,x;i<=n;++i){if(vis[i]||vis[i+n]) continue;for(int j=1;j<=2*n;++j){if(fa[j]==fa[i]){vis[j]=1;if(j<=n) ans.push_back(j);}}}cout<<ans.size()<<"\n";for(auto it:ans) cout<<it<<" ";cout<<"\n";return 0;
}
CF292D Connected Components
好题。由于每次不能用编号为 \(x \sim y\) 的边,那么我们可以暴力加入编号 \(1 \sim x-1\) 与 \(y+1 \sim n\) 的边,时间复杂度为 \(O(mk)\),暴力做的话优化一下常数还是过得去的。
有什么优化?我们可以对于并查集进行前后缀优化,建立 \(n\) 个并查集,第 \(i\) 个并查集表示加入前 \(i\) 条边时并查集的样子,再建立 \(n\) 个并查集,表示加后 \(i\) 条边时并查集的样子,然后每一次询问将两个并查集合并在一起即可,时间复杂度 \(O(kn)\) 乘上反阿克曼函数。
代码:
//我是彩笔,只写了暴力做法
const int N=505,M=10005;
int n,m,q,x,y,sum;
int a[M],b[M],fa[N];inline int find(int x)
{if(x!=fa[x]) fa[x]=find(fa[x]);return fa[x];
}signed main()
{cin>>n>>m;for(int i=1;i<=m;++i) cin>>a[i]>>b[i];cin>>q;while(q--){cin>>x>>y;for(int i=1;i<=n;++i) fa[i]=i;sum=n;for(int i=1;i<x;++i){if(find(a[i])!=find(b[i])){fa[fa[a[i]]]=fa[b[i]];sum--;}}for(int i=y+1;i<=m;++i){if(find(a[i])!=find(b[i])){fa[fa[a[i]]]=fa[b[i]];sum--;}}cout<<sum<<endl;}return 0;
}
CF982D Shark
输入一串数,要求你找到一个满足以下条件的 \(k\)。
-
所有小于 \(k\) 的数组成的连续子段长度要相等。
-
满足条件 1 的情况下段尽可能多。
-
满足前两个条件的情况下 \(k\) 尽可能小。
由于限制很多啊,我们看是否能进行操作删去一种限制,我们发现我们其实可以从小到大枚举每一个数,设其为 \(k\),那么我们就把限制 1 转化为了一个判定性问题。那么可以从小到大将数插入序列,此过程中,记录一下当前集合的数量以及每种长度集合的数量,如果当前数 \(x\) 插入后,集合数量=长度为当前数所在集合的长度的集合数量(有点绕,理清),那么就满足了限制 \(1\) ,后面两个限制都是求更优的,那么记录下最优解看是否当前能替换最优解即可。
代码:
const int M=1e5+5;
int n,num,ans,maxx;
int fa[M],vis[M],t[M],siz[M];
//num代表了集合的数量 t代表了每个大小集合的数量 struct node{int x,id;inline bool operator <(const node &o) const{return x<o.x;}
};node a[M]; inline int find(int x)
{if(x!=fa[x]) fa[x]=find(fa[x]);return fa[x];
}
inline void merge(int x,int y)
{x=find(x),y=find(y);if(x==y) return ;fa[x]=y,siz[y]+=siz[x];
}
signed main()
{ios::sync_with_stdio(false);cin.tie(0);cout.tie(0);cin>>n;for(int i=1;i<=n;++i) cin>>a[i].x,a[i].id=fa[i]=i,siz[i]=1;sort(a+1,a+n+1);//将数从小到大的插入序列for(int i=1;i<=n;++i){vis[a[i].id]=1;if(vis[a[i].id-1])//左边的数已经在序列了{--num;--t[siz[find(a[i].id-1)]];//因为左边所在的集合需要被合并,大小会发生改变merge(a[i].id-1,a[i].id);}if(vis[a[i].id+1])//右边的数已经在序列了{--num;--t[siz[find(a[i].id+1)]];merge(a[i].id+1,a[i].id);}++num,++t[siz[find(a[i].id)]];//合并完的集合长度扔进 t 数组中if(num==t[siz[find(a[i].id)]]&&num>maxx)//如果所有集合都是当前这个数所在的集合大小,那么就满足了限制1,如果集合数大于之前最优,那么依据第二个限制就可以更新答案{maxx=num,ans=a[i].x+1;}}cout<<ans<<"\n";return 0;
}
//1.所有小于k的数组成的段连续长度相同
//2.满足1的情况下段数最多
//3.满足2的情况下k尽量的小
CF766D Mahmoud and a Dictionary
拓展域并查集板子题,只是元素以字符串的形式给出,用 map
映射一下即可。
代码:
const int M=2e5+5;
int n,m,q;
int fa[M];
map<string,int> mapp;
string str,a,b;inline int find(int x)
{if(x!=fa[x]) fa[x]=find(fa[x]);return fa[x];
}inline void merge(int x,int y)
{x=find(x),y=find(y);if(x==y) return ;fa[x]=y;
}
signed main()
{ios::sync_with_stdio(false);cin.tie(0);cout.tie(0);cin>>n>>m>>q;for(int i=1;i<=2*n;++i) fa[i]=i;for(int i=1;i<=n;++i) cin>>str,mapp[str]=i;for(int i=1,opt,x,y;i<=m;++i){cin>>opt>>a>>b,x=mapp[a],y=mapp[b];if(opt==1){if(find(x)==find(y+n)||find(y)==find(x+n)) cout<<"NO\n";else cout<<"YES\n",merge(x,y),merge(x+n,y+n);}else{if(find(x)==find(y)) cout<<"NO\n";else cout<<"YES\n",merge(x,y+n),merge(y,x+n);}}int x,y;while(q--){cin>>a>>b,x=mapp[a],y=mapp[b];if(find(x)==find(y)) cout<<"1\n";else if(find(x)==find(y+n)||find(y)==find(x+n)) cout<<"2\n";else cout<<"3\n"; }return 0;
}
CF1619G Unusual Minesweeper
二分答案+并查集预处理。简单题,一个地雷爆炸会引爆所有与此地雷垂直和水平距离在 \(k\) 之内的地雷,(必须在同一行/同一列),那么按 \(x\) 和 \(y\) 分别排个序,然后将所有能互相引爆的地雷用并查集合并在一起,那么这个集合内所有的地雷爆炸的时间就是这个集合内最先爆炸的那个地雷。
由于还可以手动引爆地雷,那么贪心的想肯定是手动引爆自动爆炸时间大的集合。于是二分时间 \(x\),此时手动引爆了后 \(x\) 个,那么看第 \(tot-x+1\) 个集合自动爆炸的时间是否早于 \(x\) (\(tot\) 是集合的数量),二分答案。
代码:
const int M=2e5+5;
int T,n,k,tot;
int fa[M],t[M],res[M];struct N{int x,y,t,id;
};N p[M];
inline bool cmp1(N a,N b)
{return a.x!=b.x?a.x<b.x:a.y<b.y;
}
inline bool cmp2(N a,N b)
{return a.y!=b.y?a.y<b.y:a.x<b.x;
}inline int find(int x)
{if(x!=fa[x]) fa[x]=find(fa[x]);return fa[x];
}
inline void merge(int x,int y)
{x=find(x),y=find(y);if(x==y) return ;fa[x]=y,t[y]=min(t[x],t[y]);
}inline int check(int x)//x的时间内,手动清除x+1个炸弹,还剩下
{return (res[tot-x-1]<=x);
}
signed main()
{ios::sync_with_stdio(false);cin.tie(0);cout.tie(0);cin>>T;while(T--){cin>>n>>k,tot=0;for(int i=1;i<=n;++i){cin>>p[i].x>>p[i].y>>p[i].t;fa[i]=p[i].id=i,t[i]=p[i].t;}sort(p+1,p+n+1,cmp1);for(int i=2;i<=n;++i){if(p[i].x==p[i-1].x&&p[i].y-p[i-1].y<=k){merge(p[i].id,p[i-1].id);}}sort(p+1,p+n+1,cmp2);for(int i=2;i<=n;++i){if(p[i].y==p[i-1].y&&p[i].x-p[i-1].x<=k){merge(p[i].id,p[i-1].id);}}for(int i=1;i<=n;++i){if(i==find(i)) res[++tot]=t[i];}//现在res中就是各个连通块自动爆炸的时间限制sort(res+1,res+tot+1);int l=0,r=tot-1,mid;while(l<r){mid=(l+r)>>1;if(check(mid)) r=mid;else l=mid+1;}cout<<l<<"\n"; }return 0;
}
CF659F Polycarp and Hay
大模拟题,有 \(4\) 个限制:
-
所有 \(a_{i,j}>0\) 的格子形成一个连通块
-
这些格子的 \(a_{i,j}\) 值相同
-
你还需要保证所有格子的值的和 \(=k\)
-
至少有一个 \(a_{i,j}>0\) 的格子没有改变。
保证有一个 \(a_{i,j}>0\) 的格子没有改变,实际上就是让所有权值还大于 0 的点的权值都变为 \(a_{i,j}\),于是考虑从大到小的枚举这个权值且这个权值是 \(k\) 的因数(因为 \(a_{i,j}\) 相同并且和为 \(k\))。保证了权值在 \(a_{i,j}\) 中出现过,并且是 \(k\) 的因数。
然后看当前大于等于这个权值的点四联通合并之后最大的连通块是否大于等于 \(k/当前枚举的这个权值\),因为你需要让所有 \(a_{i,j}>0\) 的格子形成一个连通块,如果大于等于了,那么我们就可以在这个最大的权值中保留一些点来使得权值和等于 \(k\)。
注意,你至少需要保留一个原本权值就是当前枚举权值的点(满足第 4 个限制)。
代码:
#define pii pair<int,int>
#define mk make_pair
#define int long long
using namespace std;
inline int max(int x,int y){return x>y?x:y;}
inline int min(int x,int y){return x>y?y:x;}
const int M=1e3+5;
int n,m,k,tot;
int a[M][M],fa[M*M],vis[M][M],w[M*M],siz[M*M];
vector<pii> t[M*M];
vector<int> s;
map<int,int> mapp;
int fx[5]={0,1,-1,0,0},fy[5]={0,0,0,1,-1};inline int pos(int i,int j){return (i-1)*m+j;}
inline int find(int x)
{if(x!=fa[x]) fa[x]=find(fa[x]);return fa[x];
}
inline void merge(int x,int y)
{x=find(x),y=find(y);if(x==y) return ;fa[x]=y,siz[y]+=siz[x],s.push_back(siz[y]);
}
inline void insert(int x,int y)
{vis[x][y]=1,s.push_back(1);for(int i=1,sx,sy;i<=4;++i){sx=x+fx[i],sy=y+fy[i];if(sx<1||sx>n||sy<1||sy>m||!vis[sx][sy]) continue;merge(pos(x,y),pos(sx,sy));}
}queue<pii> q;
inline void print(int limit,int num,int maxx)
{cout<<"YES\n";int flag=1;for(int i=1;i<=n;++i){for(int j=1;j<=m;++j){vis[i][j]=0;if(a[i][j]==limit&&siz[find(pos(i,j))]==maxx&&flag){q.push(mk(i,j)),flag=0,--num,vis[i][j]=1;} }}while(!q.empty()){int x=q.front().first,y=q.front().second;q.pop();for(int i=1,sx,sy;i<=4;++i){sx=x+fx[i],sy=y+fy[i];if(sx<1||sx>n||sy<1||sy>m||vis[sx][sy]||!num||a[sx][sy]<limit) continue;--num,vis[sx][sy]=1,q.push(mk(sx,sy));}}for(int i=1;i<=n;++i){for(int j=1;j<=m;++j){if(vis[i][j]) cout<<limit<<" ";else cout<<"0 ";}cout<<"\n";}
}signed main()
{ios::sync_with_stdio(false);cin.tie(0);cout.tie(0);cin>>n>>m>>k;for(int i=1;i<=n;++i){for(int j=1;j<=m;++j){cin>>a[i][j];mapp[a[i][j]]=siz[pos(i,j)]=1,fa[pos(i,j)]=pos(i,j);}}for(auto &it:mapp) it.second=++tot,w[tot]=it.first;for(int i=1;i<=n;++i){for(int j=1;j<=m;++j) t[mapp[a[i][j]]].push_back(mk(i,j));}for(int i=tot,sum=0;i>=1;--i){sum+=t[i].size(),s.clear();for(auto it:t[i]) insert(it.first,it.second);if(k%w[i]!=0) continue;sort(s.begin(),s.end());int maxx=s[s.size()-1];if(k/w[i]<=maxx) {print(w[i],k/w[i],maxx);return 0;}}cout<<"NO\n";return 0;
}
CF195E Building Forest
带权并查集板子题。
代码:
const int M=1e5+5,mod=1e9+7;
int n,ans;
int fa[M],deep[M];inline int find(int x)
{if(x==fa[x]) return x;int fax=find(fa[x]);deep[x]=(deep[x]+deep[fa[x]])%mod;fa[x]=fax;return fa[x];
}signed main()
{ios::sync_with_stdio(false);cin.tie(0);cout.tie(0);cin>>n;for(int i=1;i<=n;i++) fa[i]=i;for(int i=1,k,v,w,root;i<=n;i++){cin>>k;for(int j=1;j<=k;j++){cin>>v>>w,root=find(v);fa[root]=i;deep[root]=(deep[v]+w)%mod;ans=(ans+deep[root])%mod;}}cout<<(ans%mod+mod)%mod<<"\n";return 0;
}
CF609E Minimum spanning tree for each edge
次小生成树的同类做法,由于你需要求出强制保留某条边在树上时生成的最小生成树。那么先不考虑限制跑最小生成树,原本就在树上的边强制保留,答案肯定还是最小生成树的边权和。
但如果对于一条非树边 \(x \to y\),考虑强制将其加入进树中,那么必然会形成一个包含 \(x \to y\) 路径上所有点的环,此时要让图变为树,必然要在环上断开一条边,由于加入的这条边强制不能断,还要使得断了后这颗树边权和最小。
那么贪心的想,一定断的就是边权最大的边,于是问题就转化为了对于每一条边 \(x \to y\) ,求最小生成树上 \(x \to y\) 路径上边权的最大值,板子树剖+线段树即可。于是非树边的答案就是最小生成树的权值+这条边的边权-树上路径的最大边权。
代码:
#define ull long long
const int M=2e5+5;
int n,m;
int F[M];
ull sum,ans[M];int cnt=0;
struct node{int u,v,w,id;inline bool operator <(const node &o) const{return w<o.w;}
};node a[M];
struct N{int to,next,val;
};N p[M<<1];
int head[M];inline int find(int x)
{if(x!=F[x]) F[x]=find(F[x]);return F[x];
}
inline void add(int a,int b,int c)
{++cnt;p[cnt].next=head[a];head[a]=cnt;p[cnt].to=b,p[cnt].val=c;
}int fa[M],siz[M],son[M],deep[M],w[M];
inline void dfs1(int u,int f,int d)
{fa[u]=f,siz[u]=1,deep[u]=d;for(int i=head[u];i!=0;i=p[i].next){int v=p[i].to;if(v==f) continue;w[v]=p[i].val;dfs1(v,u,d+1);siz[u]+=siz[v];if(siz[v]>siz[son[u]]) son[u]=v;}
}int top[M],num,id[M],wt[M];
inline void dfs2(int u,int topp)
{top[u]=topp,id[u]=++num,wt[num]=w[u];if(!son[u]) return ;dfs2(son[u],topp);for(int i=head[u];i!=0;i=p[i].next){int v=p[i].to;if(top[v]) continue;dfs2(v,v);}
}int tree[M<<2],res=0;
inline void build(int u,int ll,int rr)
{if(ll==rr){tree[u]=wt[ll];return ;}int mid=(ll+rr)>>1;build(u<<1,ll,mid),build(u<<1|1,mid+1,rr);tree[u]=max(tree[u<<1],tree[u<<1|1]);
}
inline void query(int u,int ll,int rr,int L,int R)
{if(L<=ll&&rr<=R){res=max(res,tree[u]);return ;}int mid=(ll+rr)>>1;if(mid>=L) query(u<<1,ll,mid,L,R);if(R>mid) query(u<<1|1,mid+1,rr,L,R);
}
inline int ask(int x,int y)
{res=0;while(top[x]!=top[y]){if(deep[top[x]]<deep[top[y]]) swap(x,y);query(1,1,n,id[top[x]],id[x]);x=fa[top[x]];}if(x==y) return res;if(deep[x]>deep[y]) swap(x,y);query(1,1,n,id[x]+1,id[y]);return res;
}signed main()
{ios::sync_with_stdio(false);cin.tie(0);cout.tie(0);cin>>n>>m;for(int i=1;i<=n;++i) F[i]=i;for(int i=1;i<=m;++i) cin>>a[i].u>>a[i].v>>a[i].w,a[i].id=i;sort(a+1,a+m+1);for(int i=1,x,y;i<=m;++i){x=find(a[i].u),y=find(a[i].v);if(x==y) continue;F[x]=y,sum+=a[i].w;add(a[i].u,a[i].v,a[i].w),add(a[i].v,a[i].u,a[i].w); //先建出最小生成树}dfs1(1,0,1),dfs2(1,1),build(1,1,n);//边权转点权树链剖分+线段树for(int i=1;i<=m;++i) ans[a[i].id]=sum+a[i].w-ask(a[i].u,a[i].v);for(int i=1;i<=m;++i) cout<<ans[i]<<"\n";return 0;
}
CF920E Connected Components?
第一题 CF1242B 0-1 MST 的双倍经验,那题是边权为 1,但实际上还是将所有边权为 0 的边通过转化全部连起来,然后查看图中还有多少连通块。
那么这题一样的做法,用同样的处理方法将没有给定的边(没有断的边)连起来即可,同时最后统计一下每个连通块的点的个数。
代码:
const int M=2e5+5;
int n,m,minn=1;
int fa[M],vis[M],in[M],t[M],siz[M];
vector<int> e[M],res;inline int find(int x)
{if(x!=fa[x]) fa[x]=find(fa[x]);return fa[x];
}
inline void merge(int x,int y)
{x=find(x),y=find(y);if(x==y) return ;fa[x]=y,siz[y]+=siz[x];
}signed main()
{ios::sync_with_stdio(false);cin.tie(0);cout.tie(0);cin>>n>>m;for(int i=1;i<=n;++i) fa[i]=i,siz[i]=1;for(int i=1,a,b;i<=m;++i){cin>>a>>b,++in[a],++in[b];e[a].push_back(b),e[b].push_back(a);}for(int i=1;i<=n;++i) if(in[minn]>in[i]) minn=i;for(auto v:e[minn]) vis[v]=1;for(int i=1;i<=n;++i) if(!vis[i]) merge(i,minn);for(int i=1;i<=n;++i){if(!vis[i]) continue;for(auto v:e[i]) t[v]=i;t[i]=i;for(int j=1;j<=n;j++){if(t[j]==i) continue;merge(i,j);}}for(int i=1;i<=n;++i){if(find(i)==i) res.push_back(siz[i]);}sort(res.begin(),res.end());cout<<res.size()<<"\n";for(auto it:res) cout<<it<<" ";return 0;
}
CF1278D Segment Tree
set 二维数点 + 并查集判连通块数量(话说这个应该是偏向数据结构部分)
代码:
const int M=1e6+5;
int n,ans;
int pos[M],fa[M];
bool t[M];
set<pii> s;struct N{int l,r;
};N p[M];
inline int find(int x)
{if(x!=fa[x]) fa[x]=find(fa[x]);return fa[x];
}
inline void merge(int x,int y)
{x=find(x),y=find(y);if(x==y) {cout<<"NO\n";exit(0);};fa[x]=y,++ans;
}signed main()
{ios::sync_with_stdio(false);cin.tie(0);cout.tie(0);cin>>n;for(int i=1;i<=n;++i) fa[i]=i;for(int i=1;i<=n;++i){cin>>p[i].l>>p[i].r;pos[p[i].l]=pos[p[i].r]=i,t[p[i].l]=1;}for(int i=1;i<=2*n;++i){if(t[i]){auto end=s.lower_bound(mk(p[pos[i]].r,0));if(end==s.begin()) {s.insert(mk(p[pos[i]].r,pos[i]));continue;}for(auto it=s.begin();it!=end;it++) merge(pos[i],it->second);s.insert(mk(p[pos[i]].r,pos[i]));}else s.erase(mk(p[pos[i]].r,pos[i]));}cout<<(ans==n-1?"YES":"NO")<<"\n";return 0;
}
CF1108F MST Unification
给定一张无向连通图,边有边权,你每次操作可以将任意一条边的边权加一,求使得该图 MST 唯一的最小操作数,但修改操作不能改变 MST 的权值和。
也是与求次小生成树的思路类似。首先肯定要建出一颗最小生成树来,那么我们就以这颗最小生成树作为最后唯一的 MST,思考什么时候可能出现多种最小生成树的结构,对于非树边 \(x \to y\) 如果将其强制加进最小生成树中,如果最后得到的权值相同,那么这就是另一种最小生成树的结构了。
参照 CF609E Minimum spanning tree for each edge 的做法,强制加入一条边 \(x \to y\) 进最小生成树后得到的最小生成树权值=最小生成树权值+该边权值- \(x \to y\) 路径上的最大边权。(采用树剖+线段树维护)
也就是说只要非树边 \(x \to y\) 的权值= \(x \to y\) 路径上的最大边权,那么我们肯定就需要将 \(x \to y\) 这条边进行一次操作,然后它加进去后生成树的权值就一定会变大。
枚举每一条非树边,看满足条件的边的数量就是答案
代码:
const int M=2e5+5;
int n,m,ans;
int F[M];int cnt=0;
struct node{int u,v,w,flag;inline bool operator <(const node &o) const{return w<o.w;}
};node a[M];
struct N{int to,next,val;
};N p[M<<1];
int head[M];
inline int find(int x)
{if(x!=F[x]) F[x]=find(F[x]);return F[x];
}
inline void add(int a,int b,int c)
{//cout<<a<<" "<<b<<" "<<c<<"!\n";++cnt;p[cnt].next=head[a];head[a]=cnt;p[cnt].to=b,p[cnt].val=c;
}int siz[M],son[M],fa[M],deep[M],w[M];
inline void dfs1(int u,int f,int d)
{deep[u]=d,siz[u]=1,fa[u]=f;for(int i=head[u];i!=0;i=p[i].next){int v=p[i].to;if(v==f) continue;w[v]=p[i].val;dfs1(v,u,d+1);siz[u]+=siz[v];if(siz[v]>siz[son[u]]) son[u]=v; }
}int id[M],num,top[M],wt[M];
inline void dfs2(int u,int topp)
{top[u]=topp,id[u]=++num,wt[num]=w[u];if(!son[u]) return ;dfs2(son[u],topp);for(int i=head[u];i!=0;i=p[i].next){int v=p[i].to;if(top[v]) continue;dfs2(v,v);}
}int tree[M<<2],res;
inline void build(int u,int ll,int rr)
{if(ll==rr){tree[u]=wt[ll];return ;}int mid=(ll+rr)>>1;build(u<<1,ll,mid),build(u<<1|1,mid+1,rr);tree[u]=max(tree[u<<1],tree[u<<1|1]);
}
inline void query(int u,int ll,int rr,int L,int R)
{if(L<=ll&&rr<=R) {res=max(res,tree[u]);return ;}int mid=(ll+rr)>>1;if(mid>=L) query(u<<1,ll,mid,L,R);if(R>mid) query(u<<1|1,mid+1,rr,L,R);
}
inline int ask(int x,int y)
{res=0;while(top[x]!=top[y]){if(deep[top[x]]<deep[top[y]]) swap(x,y);query(1,1,n,id[top[x]],id[x]);x=fa[top[x]];}if(deep[x]>deep[y]) swap(x,y);query(1,1,n,id[x]+1,id[y]);return res;
}signed main()
{ios::sync_with_stdio(false);cin.tie(0);cout.tie(0);cin>>n>>m;for(int i=1;i<=n;++i) F[i]=i;for(int i=1;i<=m;++i) cin>>a[i].u>>a[i].v>>a[i].w;sort(a+1,a+m+1);for(int i=1,x,y;i<=m;++i){x=find(a[i].u),y=find(a[i].v);if(x==y) continue;F[x]=y,a[i].flag=1;add(a[i].u,a[i].v,a[i].w),add(a[i].v,a[i].u,a[i].w);}dfs1(1,0,1),dfs2(1,1);build(1,1,n);for(int i=1;i<=m;++i){if(a[i].flag) continue;ans+=(ask(a[i].u,a[i].v)==a[i].w);}cout<<ans<<"\n";return 0;
}
CF466E Information Graph
神题,但是可能并查集只是起预处理的作用(可以略过这道题)。有三种操作:
-
\(y\) 成为了 \(x\) 的上司( \(x\) 在那之前不会有上司)。
-
编号为x的员工得到了一份文件(文件编号为上一份文件编号+1,第一份文件编号为1),然后 \(x\) 把文件传给了他的上司,然后上司又传给了他的上司,以此类推,直到某人没有上司,将文件销毁。
-
查询员工 \(x\) 是否阅读过文件 \(i\)
由于并查集虽然相当于是一棵树,但是在合并过程中树的形态会发生变化,所以在线实现 \(2\) 或 \(3\) 操作不太现实(总不能写 LCT 吧),考虑离线下来,将 1 操作在线先做完,对于 2 操作离线为一条链(因为看到第 \(i\) 份文件的人实际上就是当前 \(x\) 到 \(root_x\)),然后 3 操作离线的时候相当于就是查询一个点是否在某条链上,这个有很多种方法做啊。
代码:
const int M=1e5+5;
int n,q;
int fa[M],f[M][18];int tot=0,num=0,cnt=0;
struct node{int l,r;
};node t[M];
struct query{int x,pos;
};query s[M];
struct N{int to,next;
};N p[M<<1];
int head[M],deep[M],vis[M];
inline void add(int a,int b)
{++cnt;p[cnt].next=head[a];head[a]=cnt;p[cnt].to=b;
}inline int find(int x)
{if(x!=fa[x]) fa[x]=find(fa[x]);return fa[x];
}
inline void merge(int x,int y)
{int fx=find(x),fy=find(y);fa[fx]=fy,add(x,y),add(y,x);
}
inline void dfs(int u,int fx)
{f[u][0]=fx,deep[u]=deep[fx]+1,vis[u]=1;for(int i=head[u];i!=0;i=p[i].next){int v=p[i].to;if(v==fx) continue;dfs(v,u); }
}
inline int lca(int x,int y)
{if(deep[x]<deep[y]) swap(x,y);for(int i=17;i>=0;--i){if(deep[f[x][i]]>=deep[y]) x=f[x][i];}if(x==y) return x;for(int i=17;i>=0;--i){if(f[x][i]!=f[y][i]) x=f[x][i],y=f[y][i];}return f[x][0];
}signed main()
{ios::sync_with_stdio(false);cin.tie(0);cout.tie(0);cin>>n>>q;for(int i=1;i<=n;++i) fa[i]=i;for(int i=1,opt,a,b;i<=q;++i){cin>>opt;if(opt==1) cin>>a>>b,merge(a,b);else if(opt==2) cin>>a,b=find(a),t[++tot]=(node){a,b};else cin>>a>>b,s[++num]=(query){a,b};}for(int i=1;i<=n;++i) if(!vis[i]) dfs(i,0);for(int i=1;i<18;++i){for(int j=1;j<=n;++j) f[j][i]=f[f[j][i-1]][i-1];}int lca_ab,lca_ac,lca_bc;for(int i=1,pos,maxx,minn;i<=num;++i){pos=s[i].pos;if(s[i].x==t[pos].l||s[i].x==t[pos].r) {cout<<"YES\n";continue;}if(find(t[pos].l)!=find(t[pos].r)) {cout<<"NO\n";continue;}if(find(t[pos].l)!=find(s[i].x)) {cout<<"NO\n";continue;}lca_ab=lca(t[pos].l,t[pos].r);lca_ac=lca(t[pos].l,s[i].x);lca_bc=lca(t[pos].r,s[i].x);if(deep[s[i].x]<deep[lca_ab]){cout<<"NO\n";continue;}if(lca_ac==s[i].x||lca_bc==s[i].x){cout<<"YES\n";}else cout<<"NO\n";}return 0;
}
CF190E Counter Attack
CF920E Connected Components & CF1242B 0-1 MST 的多倍经验,这次询问的是每个集合内的点分别是哪些。
一样的做法,最后将一个集合的点放进一个 vector
即可。
代码:
const int M=5e5+5;
int n,m,minn=1,tot=0;
int fa[M],vis[M],in[M],t[M],pos[M];
vector<int> e[M],res[M];inline int find(int x)
{if(x!=fa[x]) fa[x]=find(fa[x]);return fa[x];
}
inline void merge(int x,int y)
{x=find(x),y=find(y);if(x==y) return ;fa[x]=y;
}signed main()
{ios::sync_with_stdio(false);cin.tie(0);cout.tie(0);cin>>n>>m;for(int i=1;i<=n;++i) fa[i]=i;for(int i=1,a,b;i<=m;++i){cin>>a>>b,++in[a],++in[b];e[a].push_back(b),e[b].push_back(a);}for(int i=1;i<=n;++i) if(in[minn]>in[i]) minn=i;for(auto v:e[minn]) vis[v]=1;for(int i=1;i<=n;++i) if(!vis[i]) merge(i,minn);for(int i=1;i<=n;++i){if(!vis[i]) continue;for(auto v:e[i]) t[v]=i;t[i]=i;for(int j=1;j<=n;j++){if(t[j]==i) continue;merge(i,j);}}for(int i=1;i<=n;++i){if(find(i)==i) pos[i]=++tot;}for(int i=1;i<=n;++i){res[pos[find(i)]].push_back(i);}cout<<tot<<"\n";for(int i=1;i<=tot;++i){cout<<res[i].size()<<" ";for(auto it:res[i]) cout<<it<<" ";cout<<"\n";}return 0;
}
CF1156D 0-1-Tree
很妙的题目。给定一棵 \(n\) 个点的边权为 \(0\) 或 \(1\) 的树,一条合法的路径 \((x,y)\)(\(x \neq y\))满足,从 \(x\) 走到 \(y\),一旦经过边权为 \(1\) 的边,就不能再经过边权为 \(0\) 的边,求有多少路径满足条件?
我们先不考虑走两种边权都含的路径,只考虑全走边权为 \(0/1\) 的边,于是建两个并查集,把全以 \(1\) 为边的连通块和全以 \(0\) 为边的连通块找出来,那么对于一个大小为 \(siz_x\) 的连通块,路径数量自然是 \(siz_x*(siz_x-1)\)。
然后考虑先走边权为\(1\) 的边然后走边权为 \(0\) 的边,那么枚举每个点为中转点,这样路径的数量就是当前点所在的(全 \(1\) 连通块的大小- \(1\))* (全 \(0\) 连通块的大小- \(1\))。
代码:
const int M=2e5+5;
int n;struct dsu{int fa[M],siz[M];inline void build(int n){for(int i=1;i<=n;++i) fa[i]=i,siz[i]=1;}inline int find(int x){if(x!=fa[x]) fa[x]=find(fa[x]);return fa[x];}inline void update(int x,int y){x=find(x),y=find(y);siz[y]+=siz[x],fa[x]=y;}inline int query(int x) {return siz[find(x)];}
};dsu tree1,tree2;signed main()
{ios::sync_with_stdio(false);cin.tie(0);cout.tie(0);cin>>n;tree1.build(n),tree2.build(n);for(int i=1,x,y,z;i<n;++i){cin>>x>>y>>z;if(!z) tree1.update(x,y);else tree2.update(x,y);}int ans=0;for(int i=1,x,y;i<=n;++i){x=tree1.query(i),y=tree2.query(i);if(i==tree1.find(i)) ans+=x*(x-1);//全0连通块if(i==tree2.find(i)) ans+=y*(y-1);//全1连通块ans+=(x-1)*(y-1);/当前点作为中转点的贡献}cout<<ans<<"\n";
}
CF1618G Trader Problem
其实这题和上一篇博客中几道,给定一个序列,可以交换某些位置,然后输出字典序最小/大的题目是类似的。由于对于一个价值为 \(x\) 的物品可以换取一个价值不超 \(x+k\) 的物品,并且换取的操作不限量,那么相当于就是将每个价值为 \(x\) 的点与价值不超过 \(x+k\) 的点合并,然后合并完之后,选取当前集合中权值最大的前 \(n\) 个。
但是这题没有那么简单,其 \(n,m \leq 2e5\) 且 \(k\) 的值是变化的。首先我们可以将每次询问离线下来,将 \(k\) 从小到大排序,因为答案肯定是随着 \(k\) 的增大而增大,具有单调性。我们再将所有物品全部放在一起,按权值也从小到大排序,因为题目中给定的 \(k\) 相当于就是两个数的差值不超过 \(k\),所以物品从小到大排序之后,我们就可以算出它们相邻两个的差值,也就是这两个物品合并起来所需的 \(k\) 值(由于两个值是相邻的,所以合并需要的 \(k\) 值一定是最小的)。
于是对于每一个差值,用 vector
存一下(但是因为差值可能很大,vector
开不下,所以就用 map
,让差值去映射一个 vector
),那么 \(k\) 从小到大排序,map
也按照差值从小到大存入,于是随着 \(k\) 的增加同时将符合条件的 map
内 vector
存的可合并的数合并起来,每个集合选取的数自然是这个集合内编号小于等于 \(n\) 的数,记为\(siz\),取这个集合前 \(siz\) 个数即可。
代码:
const int M=4e5+5;
int n,m,q,res;
int sum[M],siz[M],fa[M],ans[M];
map<int,vector<int>> mapp;int cnt=0;
struct node{int x,opt;inline bool operator <(const node &o) const{return x<o.x;}
};node a[M];
struct query{int x,id;inline bool operator <(const query &o) const{return x<o.x;}
};query s[M];
inline int find(int x)
{if(x!=fa[x]) fa[x]=find(fa[x]);return fa[x];
}
signed main()
{ios::sync_with_stdio(false);cin.tie(0);cout.tie(0);cin>>n>>m>>q;for(int i=1,x;i<=n;++i) cin>>x,a[++cnt]=(node){x,cnt};for(int i=1,x;i<=m;++i) cin>>x,a[++cnt]=(node){x,cnt};sort(a+1,a+cnt+1);for(int i=1;i<=cnt;++i){sum[i]=sum[i-1]+a[i].x,siz[i]=(a[i].opt<=n);//这里的siz记录的是集合内编号小于等于n的数,统计答案时就选取这个集合内前siz大的数,fa[i]=i;if(a[i].opt<=n) res+=a[i].x;if(i>1) mapp[a[i].x-a[i-1].x].push_back(i-1);}for(int i=1,x;i<=q;++i) cin>>x,s[i]=(query){x,i};sort(s+1,s+q+1),ans[0]=res;//将k离线下来,一开始的答案就是给定的n个数之和auto it=mapp.begin();for(int i=1;i<=q;++i){if(s[i].x==s[i-1].x) {ans[s[i].id]=ans[s[i-1].id];continue;}while(it!=mapp.end()&&it->first<=s[i].x)//如果当前差值小于当前的k,就把vector存的{for(auto pos:it->second){int fx=find(pos),fy=find(pos+1);if(fx==fy) continue;res-=(sum[fx]-sum[fx-siz[fx]]+sum[fy]-sum[fy-siz[fy]]);//由于一个集合内的数实际上是从小到大排序后序列的一段,可以用前缀和预处理,快速查询一段区间(最优肯定都是选取后面的那段区间,因为权值从小到大)siz[fy]+=siz[fx],fa[fx]=fy,res+=sum[fy]-sum[fy-siz[fy]];}it++;}ans[s[i].id]=res;}for(int i=1;i<=q;++i) cout<<ans[i]<<"\n";cout<<"\n";return 0;
}
CF733F Drivers Dissatisfaction
次小生成树思想题。每条边有边权,并且可以使用一定的代价使某一条边的边权减少 \(1\),求在代价不超过 \(S\) 的情况下,最小生成树的权值和是多少。
那么显然,肯定有一条最优边(花费的代价效率最高),所以说我们实际上只会修改一条边的权值(无论这条边在不在最小生成树上)。那么先将最小生成树建出来,对于树边,减少的权值就是 \(S/c_i\)。而对于非树边 \(x \to y\),首先算出其使用代价后得到的权值,然后强制将这条边放在最小生成树上,然后用树剖+线段树得出最小生成树上 \(x \to y\) 路径上边权的最大值,减少的代价就是这个求得的边权最大值与 \(x \to y\) 这条边使用代价后得到权值的差。
每条边遍历一遍,记录下减少权值最大的是 \(res\),然后用最小生成树的权值和减去 \(res\),就是最后的答案。
代码:
#define inf 1e18
#define int long long
#define pii pair<int,int>
#define mk make_pair
using namespace std;
const int M=2e5+5;
int n,m,S,tot,root;
int F[M];int cnt=0;
struct edge{int u,v,w,c,opt,idx;inline bool operator < (const edge &o){return w<o.w;}
};edge e[M];
struct N{int to,next,val,idx;
};N p[M<<1];
int head[M];
int fa[M],siz[M],son[M],deep[M];
int w[M],wt[M],id[M],top[M],num,dfn[M],ys[M];
inline int find(int x)
{if(F[x]!=x) F[x]=find(F[x]);return F[x];
}
inline void add(int a,int b,int c,int d)
{++cnt;p[cnt].next=head[a];head[a]=cnt;p[cnt].to=b,p[cnt].val=c,p[cnt].idx=d;
}
inline void dfs1(int u,int f,int d)
{deep[u]=d,siz[u]=1,fa[u]=f;for(int i=head[u];i!=0;i=p[i].next){int v=p[i].to;if(v==f) continue;w[v]=p[i].val,ys[v]=p[i].idx;dfs1(v,u,d+1);siz[u]+=siz[v];if(siz[v]>siz[son[u]]) son[u]=v;}
}
inline void dfs2(int u,int topp)
{top[u]=topp,id[u]=++num,wt[num]=w[u],dfn[num]=ys[u];if(!son[u]) return ;dfs2(son[u],topp);for(int i=head[u];i!=0;i=p[i].next){int v=p[i].to;if(top[v]) continue;dfs2(v,v);}
}
pii tree[M<<2],maxx;
inline void build(int u,int ll,int rr)
{tree[u]=mk(-inf,0);if(ll==rr){tree[u]=mk(wt[ll],dfn[ll]);return ;}int mid=(ll+rr)>>1;build(u<<1,ll,mid),build(u<<1|1,mid+1,rr);tree[u]=tree[u<<1].first>=tree[u<<1|1].first?tree[u<<1]:tree[u<<1|1];
}
inline pii query(int u,int ll,int rr,int L,int R)
{if(L<=ll&&rr<=R) return tree[u];int mid=(ll+rr)>>1;pii res=mk(-inf,0);if(mid>=L){maxx=query(u<<1,ll,mid,L,R);res=res.first>=maxx.first?res:maxx;}if(R>mid){maxx=query(u<<1|1,mid+1,rr,L,R);res=res.first>=maxx.first?res:maxx;}return res;
}
inline pii ask(int x,int y)
{pii ans=mk(-inf,0);while(top[x]!=top[y]){if(deep[top[x]]<deep[top[y]]) swap(x,y);maxx=query(1,1,n,id[top[x]],id[x]);ans=ans.first>=maxx.first?ans:maxx;x=fa[top[x]];}if(deep[x]>deep[y]) swap(x,y);maxx=query(1,1,n,id[x]+1,id[y]);ans=ans.first>=maxx.first?ans:maxx;return ans;
}int ans=inf,idx,pos;
signed main()
{ios::sync_with_stdio(false);cin.tie(0);cout.tie(0);cin>>n>>m;for(int i=1;i<=n;++i) F[i]=i;for(int i=1;i<=m;++i) cin>>e[i].w,e[i].idx=i;for(int i=1;i<=m;++i) cin>>e[i].c;for(int i=1;i<=m;++i) cin>>e[i].u>>e[i].v;cin>>S;sort(e+1,e+m+1);for(int i=1,x,y;i<=m;++i){x=find(e[i].u),y=find(e[i].v);if(x==y) continue;F[x]=y,e[i].opt=1,tot+=e[i].w;add(e[i].u,e[i].v,e[i].w,e[i].idx),add(e[i].v,e[i].u,e[i].w,e[i].idx);}dfs1(1,0,1),dfs2(1,1);build(1,1,n);for(int i=1,x;i<=m;++i){if(!e[i].opt){pii ctt=ask(e[i].u,e[i].v);x=tot-ctt.first+e[i].w-S/e[i].c;if(x<ans) pos=i,ans=x,idx=ctt.second;}else{if(tot-S/e[i].c<ans) pos=i,ans=tot-S/e[i].c;}}if(!e[pos].opt){e[pos].opt=1;e[pos].w-=S/e[pos].c;cout<<ans<<"\n";for(int i=1;i<=m;++i){if(e[i].opt&&e[i].idx!=idx) cout<<e[i].idx<<" "<<e[i].w<<"\n";}}else{e[pos].w-=S/e[pos].c;cout<<ans<<"\n";for(int i=1;i<=m;++i) {if(e[i].opt) cout<<e[i].idx<<" "<<e[i].w<<"\n";}}return 0;
}
CF915F Imbalance Value of a Tree
经典,和前面 CF437D The Child and Zoo & CF1213G Path Queries 思路类似。
考虑将最大值和最小值分开求解,最大值就是 CF437D The Child and Zoo,最小值的做法同最大值,边权为连接两点的点权较小值,然后边按边权从小到大排序。
考虑对于边 \(x \to y\),\(x\) 所在的连通块大小为 \(siz_x\),\(y\) 所在的连通块大小为 \(siz_y\) ,原本 \(x\) 连通块内的点与 \(y\) 连通块内的点都不连通,但是加入这条边之后就连通了,于是有 \(siz_x*siz_y\) 个点对(从 \(x\) 中选一个,从 \(y\) 中选一个,乘法原理)的 \(f\) 值就是这条边的边权 \(w\),那么这条边产生的贡献就是 \(siz_x*siz_y*w\)。(把上面的 copy 了一遍)。
代码:
#include<iostream>
#include<cmath>
#include<cstring>
#include<algorithm>
#include<queue>
#include<cstdio>
#define ll long long
using namespace std;
inline int max(int x,int y){return x>y?x:y;}
inline int min(int x,int y){return x>y?y:x;}
const int M=1e6+5;
int n;
int fa[M],vis[M];
ll siz[M],res1,res2;
vector<int> e[M];struct node{int x,id;
};node a[M];
inline bool cmp1(node a,node b) {return a.x<b.x;}
inline bool cmp2(node a,node b) {return a.x>b.x;}inline int find(int x)
{if(x!=fa[x]) fa[x]=find(fa[x]);return fa[x];
}
inline void merge(int x,int y,int w,ll &res)
{x=find(x),y=find(y);if(x==y) return ;fa[x]=y,res+=siz[x]*siz[y]*w,siz[y]+=siz[x];
}signed main()
{ios::sync_with_stdio(false);cin.tie(0);cout.tie(0);cin>>n;for(int i=1;i<=n;++i) cin>>a[i].x,a[i].id=i;for(int i=1,u,v;i<n;++i){cin>>u>>v;e[u].push_back(v),e[v].push_back(u);}for(int i=1;i<=n;++i) siz[i]=1,fa[i]=i,vis[i]=0;sort(a+1,a+n+1,cmp1);for(int i=1,pos;i<=n;++i){pos=a[i].id,vis[pos]=1;for(auto v:e[pos]){if(vis[v]) merge(pos,v,a[i].x,res1);}}//最小值for(int i=1;i<=n;++i) siz[i]=1,fa[i]=i,vis[i]=0;//需要重新初始化一下并查集sort(a+1,a+n+1,cmp2);for(int i=1,pos;i<=n;++i){pos=a[i].id,vis[pos]=1;for(auto v:e[pos]){if(vis[v]) merge(pos,v,a[i].x,res2);}}//最大值cout<<res1-res2<<"\n";return 0;
}
CF1027F Session in BSU
非常神秘的题啊,首先由于时间的跨度较大,且并不注重时间的绝对大小,所以我们首先先将时间全部离散化一下,对于每一个时间都将其看作是一个点,对于一个物品有两个时间可以参与秒杀,那么我们就将这两个时间对应的点合并起来,对于两个集合,每一次成功合并,那么就说明会多出一个时间了秒杀物品,于是记录一下有多少的时间,最后每一个集合的时间与集合的物品做一个比较。
若时间大于等于物品那么肯定就可以将所有物品秒杀。然后对于需要的时间这个也比较容易,将每个属于这个集合的时间按先后顺序加入其所在集合的 vector
中,然后加入当前集合内有 \(x\) 个物品,那么用前 \(x\) 个时间就行了,\(t[i][siz_i-1]\) 就是所用的时间,取最大值。(注意这是离散化后的相对时间大小,所以要对应回去)
代码:
#include<iostream>
#include<cmath>
#include<cstring>
#include<algorithm>
#include<queue>
#include<cstdio>
using namespace std;
inline int max(int x,int y){return x>y?x:y;}
inline int min(int x,int y){return x>y?y:x;}
const int M=1e6+5;
int n,len,ans;
int a[M],b[M],c[M<<1],fa[M<<1],siz[M<<1];
vector<int> t[M];inline int find(int x)
{if(x!=fa[x]) fa[x]=find(fa[x]);return fa[x];
}
inline void merge(int x,int y)
{x=find(x),y=find(y);if(x==y) {++siz[x];return ;}fa[x]=y,siz[y]+=siz[x]+1;
}signed main()
{ios::sync_with_stdio(false);cin.tie(0);cout.tie(0);cin>>n;for(int i=1;i<=n;++i){cin>>a[i]>>b[i];c[++len]=a[i],c[++len]=b[i];}sort(c+1,c+len+1),len=unique(c+1,c+len+1)-c-1;for(int i=1;i<=len;++i) fa[i]=i;for(int i=1;i<=n;++i){a[i]=lower_bound(c+1,c+len+1,a[i])-c;b[i]=lower_bound(c+1,c+len+1,b[i])-c;merge(a[i],b[i]);}for(int i=1;i<=len;++i) t[find(i)].push_back(i);for(int i=1;i<=len;++i){if(i==find(i)){if(siz[i]>t[i].size()) {cout<<"-1\n";return 0;}sort(t[i].begin(),t[i].end());ans=max(ans,t[i][siz[i]-1]);}}cout<<c[ans]<<"\n";return 0;
}
CF1044D Deduction Queries
非常好的带权并查集,给你 \(q\) 个操作,告诉你一个区间的异或和或问你一个区间的异或和,强制在线。
首先定义 \(f(l,r)=a_l \oplus a_{l+1} \oplus ... \oplus a_{r-1}\),根据异或的性质,于是 \(f(l,r)=f(l,k) \oplus f(k,r)\) 。于是我们就可以将 \(f(l,r)\) 理解为两个点 \(l\) 到 \(r\) 距离。
那么操作一就是告诉你两个点的距离,第二个操作就是询问两个点的距离,什么时候输出 -1
,自然是两个点不在同一个集合的时候,这时候不清楚两个点之间的距离。对于操作一,直接按照朴素的带权并查集合并即可,路径压缩就把自己上面的点的权值都异或起来。查询两个点 \(l\) 与 \(r\) 的距离,那么就找出 \(l\) 到根的距离异或上 \(r\) 到根的距离。
同时由于下标的值域很大,所以考虑离散化一下。
代码:
#include<iostream>
#include<cmath>
#include<cstring>
#include<algorithm>
#include<queue>
#include<cstdio>
#include<map>
using namespace std;
inline int max(int x,int y){return x>y?x:y;}
inline int min(int x,int y){return x>y?y:x;}
const int M=4e5+5;
int q,lastans=0,cnt=0;
int fa[M],s[M];
map<int,int> mapp;inline int find(int x)
{if(x==fa[x]) return fa[x];else{int pos=fa[x];fa[x]=find(fa[x]);s[x]=s[pos]^s[x];//路径压缩的时候,将自己上面的点的权值都异或起来,得到自己真实到根的值return fa[x];}
}signed main()
{ios::sync_with_stdio(false);cin.tie(0);cout.tie(0);cin>>q;int opt,a,b,val;while(q--){cin>>opt>>a>>b;a^=lastans,b^=lastans;if(a>b) swap(a,b);b++;if(!mapp[a]) mapp[a]=++cnt,fa[cnt]=cnt;if(!mapp[b]) mapp[b]=++cnt,fa[cnt]=cnt;//用map离散化a=mapp[a],b=mapp[b];if(opt==1){cin>>val,val^=lastans;int x=find(a),y=find(b);if(x!=y) s[x]=s[a]^s[b]^val,fa[x]=y;//朴素的合并,只不过把操作换为异或}else{int x=find(a),y=find(b);if(x==y) cout<<(lastans=s[a]^s[b])<<"\n";//两点的距离=a到根的距离^b到根的距离else lastans=1,cout<<"-1\n";}}return 0;
}
CF1508C Complete the MST
眼不眼熟,是不是特别像本博文讲解的第一道题,对于一张完全图,给定了一些边的信息,其他边都可以随便搞。有两个限制,首先要求给所有边赋权之后使得所有边的边权异或和为 \(0\),还有一个是在所有可能的赋权方案中,找到最小生成树的边权和最小值。
明显我们并不想要限制,也就是尽量避免使用给定的边,因为贪心的想,将给定的边异或和算出来,最优解就是将一条没啥用的边赋成这个异或和(因为 \(x \oplus x=0\) ),然后其他的边都赋为 \(0\),那么直接答案就是 \(0\)。所以我们就用第一题讲解的方法解出某一条给定边是否必须被使用。
如果我们必须要使用一些边,就把我们要使用的这些边统计出来,然后看有没有空闲的边去赋给定边的异或和,要是有,答案就是必须要使用的边的边权和;反之没有我们还需要将树上的某条边赋上异或和 \(x\),首先先将某条边权为 \(0\) 的边赋成 \(x\),但是我们可能某条给定边但是不是必须要使用的边的边权小于 \(x\),于是此时我们就可以用把那条赋成 \(x\) 的边从最小生成树删去,加上这条给定边但不是一定要使用的边。(当然也有条件了,加入的这条给定边必须让连通块的数量减少,也就是说加入必选边之后,拿去换的这条边不能连同一连通块的两点,同时要找到满足这些条件的边的权值最小)。
代码:
#include<iostream>
#include<cmath>
#include<cstring>
#include<algorithm>
#include<queue>
#include<cstdio>
#define int long long
using namespace std;
inline int max(int x,int y){return x>y?x:y;}
inline int min(int x,int y){return x>y?y:x;}
const int M=2e5+5;
int n,m,tot,minn=1,res,ans;
int in[M],fa[M],vis[M],flag[M];
vector<int> t[M],c;struct edge{int u,v,w,opt;inline bool operator <(const edge &o) const{return w<o.w;}
};edge e[M];
inline int find(int x)
{if(x!=fa[x]) fa[x]=find(fa[x]);return fa[x];
}
inline int merge(int x,int y)
{x=find(x),y=find(y);if(x==y) return 0;fa[x]=y;return 1;
}signed main()
{ios::sync_with_stdio(false);cin.tie(0);cout.tie(0);cin>>n>>m,tot=n*(n-1)/2,tot-=m;for(int i=1;i<=n;++i) fa[i]=i;for(int i=1;i<=m;++i){cin>>e[i].u>>e[i].v>>e[i].w;++in[e[i].u],++in[e[i].v],res^=e[i].w;t[e[i].u].push_back(e[i].v),t[e[i].v].push_back(e[i].u);}sort(e+1,e+m+1);for(int i=1;i<=n;++i) if(in[minn]>in[i]) minn=i;for(auto v:t[minn]) vis[v]=1;for(int i=1;i<=n;++i) if(!vis[i]) merge(i,minn);for(int i=1;i<=n;++i){if(!vis[i]) continue;for(auto v:t[i]) flag[v]=i;flag[i]=i;for(int j=1;j<=n;++j){if(flag[j]==i) continue;merge(i,j);}}for(int i=1;i<=n;++i){if(i==find(i)) c.push_back(i);}//以上都是第一题讲解的做法for(int i=1;i<=m;i++){if(merge(e[i].u,e[i].v)) ans+=e[i].w,e[i].opt=1;//看每条边是否需要使用,当然权值需要从小到大排序}if(tot>n-c.size()) {cout<<ans<<"\n";return 0;}//如果此时有空闲的边去赋给定边的异或和for(int i=1;i<=n;i++) fa[i]=i;for(int i=1;i<=m;i++){if(merge(e[i].u,e[i].v))//看非必须边的给定边&可以拿来替换的最小权值{if(!e[i].opt) res=min(res,e[i].w);}}cout<<ans+res<<"\n";return 0;
}
与并查集相关的毒瘤智慧题:(咕咕咕了,因为和并查集只沾了一点,还有些是Kruskal重构树,先放这)