【学习笔记】图论杂谈(一)
# 壹:【Johnson】
无负权边:直接跑 \(n\) 遍 \(dijkstra\)
这里主要讨论带负权边的情况,其主要目的就是把负权边,转化为非负权边,然后 \(v\) 遍 \(dijkstra\)
因为无向图必然存在负环,所以这里讨论有向图
显而易见的想法是对于每条边 \(e_i\) 分别加 \(k_i\)
使得原最短路在加权之后还是最短路(限制一),并且图每条边权值非负(限制二)
一:【做法】
一个做法是,建超级原点,跑 \(SPFA\) 计算原点到其他点的最短路 \(f_i\),同时判负环
对于一条边 \(e_i (u->v)\) ,它的权值加上 \(f_u - f_v\) ,可以满足上面两条限制
二:【证明】
1.【限制一】
对于 $u -> a_{1,2...n} -> v $ 的一条路径,设原路径权值和为 \(t\) ,加权后最短路的权值为 \(f_u - f_{a_1} + f_{a_1} -...- f_v + t = f_u - f_v + t\)
可以发现只与变化量 \(u , v\) 有关,所以原最短路不变
2.【限制二】
跑完 \(SPFA\) 之后,一定满足 \(f[u]+w>=f[v]\) 即 \(w+f[u]-f[v]>=0\)
三.【Code】
P5905代码
#include<bits/stdc++.h>
#define Pair pair<int,int>
#define w first
#define v second
#define inf 1e9
using namespace std;
typedef long long LL;
const int N=3010;int n;
vector<Pair> mp[N];
int f[N];
int vis[N],step[N];
bool SPFA(){
for(int i=1;i<N;i++) f[i]=inf;
queue<int> q;
q.push(0);vis[0]=1;
while(q.size()){
int u=q.front();q.pop();vis[u]=0;
for(auto e:mp[u]){
int v=e.v,w=e.w;
if(f[v]>f[u]+w){
f[v]=f[u]+w;
step[v]=step[u]+1;
if(step[v]>=n+2) return 1;//n+k保证一定不会WA
if(vis[v]) continue;
q.push(v);
vis[v]=1;
}
}
}
return 0;
}
int dis[N];
int cl(int v,int u){
if(dis[v]==inf) return dis[v];
return dis[v]+f[v]-f[u];
}
LL dijkstra(int s){
priority_queue<Pair,vector<Pair>,greater<Pair> > q;
for(int i=0;i<N;i++){
dis[i]=inf;
vis[i]=0;
}
q.push({0,s});dis[s]=0;
while(q.size()){
int u=q.top().v;q.pop();
if(vis[u]) continue;
vis[u]=1;
for(auto e:mp[u]){
int w=e.w,v=e.v;
if(dis[v]>dis[u]+w){
dis[v]=dis[u]+w;
q.push({dis[v],v});
}
}
}
LL ans=0;
for(int i=1;i<=n;i++) ans+=1LL*i*cl(i,s);
return ans;
}
int main(){
ios::sync_with_stdio(0);cin.tie(0);cout.tie(0);
int m;cin>>n>>m;
while(m--){
int u,v,w;cin>>u>>v>>w;
mp[u].push_back({w,v});
}
for(int i=1;i<=n;i++) mp[0].push_back({0,i});
if(SPFA()){
cout<<-1<<"\n";
return 0;
}
for(int u=1;u<=n;u++){
for(auto &e:mp[u]) e.w+=f[u]-f[e.v];
}
for(int u=1;u<=n;u++){
cout<<dijkstra(u)<<"\n";
}
return 0;
}
四:【EX】
当然,如果你不怕被卡,可以直接跑 \(n\) 遍 \(SPFA\)
# 贰:【最短路树】
满足以下性质的被称为最短路树:
- 原图的生成树
- 根节点到其他节点路径长度是原图中的最短路长度
可以跑\(dijkstra\),记录前驱生成
一:【最短路DAG】
设 \(u->v\) 最短路长度为 \(w\) ,把 \(u->v\) 所有长度为 \(w\) 的路径的每条边加入边集当中
对于每个 \(v\) 都做一遍,对边集去重得到的生成图,被称为最短路 \(DAG\)
最短路 \(DAG\) 的每一个生成树都是一颗最短路树
实际构造时,可以先跑一遍最短路
然后再跑一遍,记录前驱
CF545E
#include<bits/stdc++.h>
#define int long long
#define Pair pair<int,int>
#define w first
#define v second
#define inf 1e15
using namespace std;
typedef long long LL;
const int N=3e5+10;
struct node{
LL w,v,id;
};
vector<node> mp[N];
LL vis[N],dis[N];
void dijkstra(int s){
for(int i=0;i<N;i++) dis[i]=inf;
priority_queue<Pair,vector<Pair>,greater<Pair> > q;
q.push({0,s});dis[s]=0;
while(q.size()){
int u=q.top().v;q.pop();
if(vis[u]) continue;
vis[u]=1;
for(auto e:mp[u]){
int w=e.w,v=e.v;
if(dis[v]>dis[u]+w){
dis[v]=dis[u]+w;
q.push({dis[v],v});
}
}
}
}
vector<node> qq[N];
signed main(){
int n,m;cin>>n>>m;
for(int i=1;i<=m;i++){
int u,v,w;cin>>u>>v>>w;
mp[u].push_back({w,v,i});
mp[v].push_back({w,u,i});
}
int s;cin>>s;
dijkstra(s);
for(int u=1;u<=n;u++){
for(auto e:mp[u]){
int v=e.v,w=e.w;
if(dis[v]==dis[u]+w) qq[v].push_back({w,u,e.id});
}
}
LL ans=0;
vector<int> xl;
for(int u=1;u<=n;u++){
LL mn=inf,tp;
for(auto e:qq[u]){
if(e.w<mn){
mn=e.w;
tp=e.id;
}
}
if(mn==inf) continue;//root
ans+=mn;
xl.push_back(tp);
}
cout<<ans<<"\n";
sort(xl.begin(),xl.end());
for(auto v:xl) cout<<v<<" ";cout<<"\n";
return 0;
}
//建最短路DAG
//对于每个点u,在DAG中保留边权最小的入边,构造最短路树
//当然也可以直接建最短路树
二:【删边最短路】
给定一张无向图(有向应该也可以这样求解),求删除每一条边后 \(1->n\) 的最短路
记录一条最短路径 \(st\) ,如果删边不在此最短路径上,则答案不变
如果有多条最短路径,我们认为其他最短路径不为最短路径,即不做考虑,容易证明这样不会对答案造成影响
所以我们只讨论最短路径上边的答案维护
首先剖出1的最短路树 \(T1\) ,\(n\) 的最短路树 \(T2\)(注意,不是最短路 \(DAG\) )
容易猜出一个性质:每删除一条边后,\(1->n\) 的最短路径有且仅有一条边不在最短路树(即 \(T1\) 和 \(T2\) )上
证明
1.【有】如果都在最短路树上,则此路径一定是最短路径 \(st\) ,与题设矛盾
2.【仅有】假设有两条边,则有一条边一定可以被最短路径树上的边给松弛掉
对于每次删边,所以我们可以边 \((u,v,w)\) 满足其不在最短路树上,然后用 \(T1(u)+w+T2(v)\) 更新求最小值,这样做的复杂度为 \(O(m^2)\)
我们可以先枚举 \((u,v,w)\) ,在看有哪些边被删后需要用到 \((u,v,w)\) ,更新其最小值
容易发现,\(u\) 在 \(T1\) 上的祖宗不能被更新,\(v\) 在 \(T2\) 上的祖宗不能被更新,且只需要更新\(st\)上的边
所以我们可以定义\(u'=T1.LCA(u,n),v'=T2.LCA(v,1)\),更新\(u'->v'\)上的边的信息,线段树加速
复杂度\(O(mlogm)\)
总结:猜出性质,建树,枚举\((u,v,w)\),卡范围,线段树维护
P2685 [TJOI2012] 桥
#include<bits/stdc++.h>
#define Pair pair<int,int>
#define w first
#define v second
#define inf 1e9+10
using namespace std;
const int N=1e5+10,M=4*N;int n;
struct Paid{
int w,v,id;
};
vector<Paid> mp[N];
struct Edge{
int u,v,w;
}edg[M];
struct Tree{
vector<Paid> T[N];
int dis[N],vis[N];
Paid pre[N];
void Dijkstra(int s){
for(int i=0;i<N;i++){
dis[i]=inf;
vis[i]=0;
}
priority_queue<Pair,vector<Pair>,greater<Pair> > q;
q.push({0,s});dis[s]=0;
while(q.size()){
int u=q.top().v;q.pop();
if(vis[u]) continue;
vis[u]=1;
for(auto e:mp[u]){
int v=e.v,w=e.w;
if(dis[v]>dis[u]+w){
dis[v]=dis[u]+w;
q.push({dis[v],v});
pre[v]={w,u,e.id};
}
}
}
}
void make(){
for(int i=1;i<=n;i++){
if(pre[i].v==0) continue;
T[pre[i].v].push_back({pre[i].w,i,pre[i].id});
T[i].push_back({pre[i].w,pre[i].v,pre[i].id});
}
}
int son[N],top[N],fa[N],dep[N],siz[N],cnt=0;
void Son(int u,int pa){
fa[u]=pa;
dep[u]=dep[pa]+1;
siz[u]=1;
for(auto e:T[u]){
int v=e.v;
if(v==pa) continue;
Son(v,u);
if(siz[v]>siz[son[u]]) son[u]=v;
siz[u]+=siz[v];
}
}
void Line(int u,int tp){
top[u]=tp;
if(!son[u]) return ;
Line(son[u],tp);
for(auto e:T[u]){
int v=e.v;
if(v==son[u]||v==fa[u]) continue;
Line(v,v);
}
}
int LCA(int a,int b){
while(top[a]!=top[b]){
if(dep[top[a]]<dep[top[b]]) swap(a,b);
a=fa[top[a]];
}
if(dep[a]>dep[b]) swap(a,b);
return a;
}
}T1,T2;
int dfn[N];
int st[M];
int nod[M];
int tree[N<<2];
void update(int q,int l,int r,int L,int R,int d){
if(L>R) return ;
if(L<=l&&r<=R){
tree[q]=min(tree[q],d);
return ;
}
int mid=(l+r)>>1;
if(L<=mid) update(q<<1,l,mid,L,R,d);
if(mid<R) update(q<<1|1,mid+1,r,L,R,d);
}
int query(int q,int l,int r,int tp){
if(l==r) return tree[q];
int ans=tree[q];
int mid=(l+r)>>1;
if(tp<=mid) ans=min(ans,query(q<<1,l,mid,tp));
else ans=min(ans,query(q<<1|1,mid+1,r,tp));
return ans;
}
int as[M];
int main(){
int m;cin>>n>>m;
for(int i=1;i<=m;i++){
int u,v,w;cin>>u>>v>>w;
edg[i]={u,v,w};
mp[u].push_back({w,v,i});
mp[v].push_back({w,u,i});
}
for(int i=1;i<=m;i++) edg[i+m]={edg[i].v,edg[i].u,edg[i].w};
T2.Dijkstra(n);T2.make();
int cnt=0;
int now=1;
while(now!=n){
st[T2.pre[now].id]=1;
dfn[now]=++cnt;
now=T2.pre[now].v;
}dfn[n]=++cnt;
T1.Dijkstra(1);
for(int i=1;i<=n;i++){
int k=T2.pre[i].v;
if(!dfn[k]||!dfn[i]) continue;
T1.pre[k]={T2.pre[i].w,i,T2.pre[i].id};
}
T1.make();
T1.Son(1,0);T1.Line(1,1);
T2.Son(n,0);T2.Line(n,n);
for(int i=1;i<=2*m;i++){
if(!st[i]) continue;
int u=edg[i].u,v=edg[i].v;
if(T1.dep[u]>T1.dep[v]) swap(u,v);
nod[i]=u;
}
for(int i=0;i<(N<<2);i++) tree[i]=inf;
for(int i=1;i<=2*m;i++){
if(st[i]||(i>m&&st[i-m])) continue;
int u=edg[i].u,v=edg[i].v,w=T1.dis[u]+T2.dis[v]+edg[i].w;
u=T1.LCA(u,n);v=T1.fa[T2.LCA(v,1)];
update(1,1,cnt,max(1,dfn[u]),dfn[v],w);
}
for(int i=1;i<=m;i++){
if(!st[i]) as[i]=T1.dis[n];
else as[i]=query(1,1,cnt,dfn[nod[i]]);
}
int mx=0,num=0;
for(int i=1;i<=m;i++){
if(as[i]>mx){
mx=as[i];
num=1;
}
else if(as[i]==mx) num++;
}
cout<<mx<<" "<<num<<"\n";
return 0;
}
//建T2的时候,要保证T1到n的路径,和T2到1的路径重合
//维护信息的时候,把边的信息记录在点上
# 参:【平面图最小割】
平面图:除顶点外处处无边相交的图
这里只考虑网格图,平面图可以很容易被拓展
给定一张网格图,删除一组边,使得不存在一条从左上走到右下的一条路径,且删边权值和最小
一:【对偶图】
每相邻的四个点会围成一个面,我们对于每一个面建点,每相邻两个面连边,边权即为与原图交叉的边的权值
然后我们再人工把网格图外的面切割成两部分,左下建点,右上建点,分别连边
此时我们造出来了一张新图,我们称之为对偶图
例如

二:【how to 求最小割】
容易发现,对偶图的最短路即为原图的最小割
三:【EX】
也可以跑网络流
P4001 [ICPC-Beijing 2006] 狼抓兔子
#include<bits/stdc++.h>
#define Pair pair<int,int>
#define w first
#define v second
#define inf 1e9+10
using namespace std;
const int N=999*2*999+10;int n,m;
vector<Pair> mp[N];
int S,T;
int id(int x,int y,int o){
if(y==0||x==n) return S;
if(y==m||x==0) return T;
return (x-1)*(m-1)*2+2*(y-1)+1+o;
}
int dis[N],vis[N];
int main(){
cin>>n>>m;
S=0;
T=(n-1)*(m-1)*2+1;
for(int i=1;i<=n;i++){
for(int j=1;j<m;j++){
int w;cin>>w;
int a=id(i-1,j,0),b=id(i,j,1);
mp[a].push_back({w,b});
mp[b].push_back({w,a});
}
}
for(int i=1;i<n;i++){
for(int j=1;j<=m;j++){
int w;cin>>w;
int a=id(i,j-1,1),b=id(i,j,0);
mp[a].push_back({w,b});
mp[b].push_back({w,a});
}
}
for(int i=1;i<n;i++){
for(int j=1;j<m;j++){
int w;cin>>w;
int a=id(i,j,0),b=id(i,j,1);
mp[a].push_back({w,b});
mp[b].push_back({w,a});
}
}
for(int i=0;i<N;i++){
dis[i]=inf;
vis[i]=0;
}
priority_queue<Pair,vector<Pair>,greater<Pair> > q;
q.push({0,S});dis[S]=0;
while(q.size()){
int u=q.top().v;q.pop();
if(vis[u]) continue;
vis[u]=1;
for(auto e:mp[u]){
int v=e.v,w=e.w;
if(dis[v]>dis[u]+w){
dis[v]=dis[u]+w;
q.push({dis[v],v});
}
}
}
cout<<dis[T]<<"\n";
return 0;
}
//S 纵0横n
//T 纵m横0
# 肆:【Tarjan求连通性问题】
【连通分量定义】
- 满足某条性质的极大的连通块
【通用求法】
- \(Tarjan\)算法跑一遍
- 即构建\(dfs\)优先生成树,构建的过程中,记录\(low=dfn=++cnt\),同时用栈存未被标记节点
- 若\(low==dfn\),栈反复弹出直到弹到\(x\)或弹出\(x\),打上标记
- 以下复杂度都为\(O(n+m)\)
以下具体情况,具体分析
一:【点双连通分量】
基于无向图
1.【非根节点】
满足\(low==dfn\),此节点即为割点
栈反复弹出直到弹到\(x\)(\(x\)不弹出,因为\(x\)属于多个点双)
2.【根节点】
如果\(dfs\)优先生成树中,只有一个子树,则不为割点,否则为割点
当然它有几个子树,它就属于几个点双
因此实际操作的时候,不需要分类讨论,但是求割点的时候需要分类
不过由于一个点可能属于多个点双,所以写法略有不同
P8435 【模板】点双连通分量
#include<bits/stdc++.h>
#define Pair pair<int,int>
#define v first
#define id second
using namespace std;
const int N=5e5+10;int n;
vector<int> mp[N];
int low[N],dfn[N],cnt;int bccidx;
vector<int> st;
vector<int> bl[N];
void BCC(int u,int fa){
st.push_back(u);
low[u]=dfn[u]=++cnt;
if(mp[u].empty()){//独立点判断
bccidx++;
bl[bccidx].push_back(u);
return ;
}
for(auto v:mp[u]){
if(v==fa) continue;
if(!dfn[v]){
BCC(v,u);
low[u]=min(low[u],low[v]);
if(low[v]>=dfn[u]){//只考虑当前子树
bccidx++;
while(1){
int x=st.back();st.pop_back();
bl[bccidx].push_back(x);
if(x==v) break;
}
bl[bccidx].push_back(u);
}
}
else low[u]=min(low[u],dfn[v]);
}
}
void Tarjan(){
for(int i=1;i<=n;i++){
if(dfn[i]==0) BCC(i,i);
}
}
int main(){
int m;cin>>n>>m;
for(int i=1;i<=m;i++){
int a,b;cin>>a>>b;
if(a==b) continue;//独立点判断前提(去自环)
mp[a].push_back(b);
mp[b].push_back(a);
}
Tarjan();
int ans=0;
for(int i=1;i<=n;i++){
if(bl[i].size()) ans++;
}
cout<<ans<<"\n";
for(int i=1;i<=n;i++){
if(bl[i].empty()) continue;
cout<<bl[i].size()<<" ";
for(auto v:bl[i]) cout<<v<<" ";cout<<"\n";
}
return 0;
}
二:【边双连通分量】
基于无向图
一条边,一个点,最多属于一个边双,所以边双的求法要比点双简单一点
满足\(low==dfn\),此节点到父亲的边为割边(桥)
栈反复弹出直到弹出\(x\)
P8436 【模板】边双连通分量
#include<bits/stdc++.h>
#define Pair pair<int,int>
#define v first
#define id second
using namespace std;
const int N=5e5+10;int n;
vector<Pair> mp[N];
int bcc[N],low[N],dfn[N],cnt;int bccidx;
vector<int> st;
void BCC(int u,int fat){
st.push_back(u);
low[u]=dfn[u]=++cnt;
for(auto e:mp[u]){
int v=e.v,id=e.id;
if(id==fat) continue;
if(!dfn[v]){
BCC(v,e.id);
low[u]=min(low[u],low[v]);
}
else low[u]=min(low[u],dfn[v]);//无向图不需要考虑横叉边
}
if(low[u]==dfn[u]){
++bccidx;
while(1){
int x=st.back();st.pop_back();
bcc[x]=bccidx;
if(x==u) break;
}
}
}
void Tarjan(){
for(int i=1;i<=n;i++){
if(dfn[i]==0) BCC(i,0);
}
}
vector<int> bl[N];
int main(){
int m;cin>>n>>m;
for(int i=1;i<=m;i++){
int a,b;cin>>a>>b;
mp[a].push_back({b,i});
mp[b].push_back({a,i});
}
Tarjan();
for(int i=1;i<=n;i++) bl[bcc[i]].push_back(i);
int ans=0;
for(int i=1;i<=n;i++){
if(bl[i].size()) ans++;
}
cout<<ans<<"\n";
for(int i=1;i<=n;i++){
if(bl[i].empty()) continue;
cout<<bl[i].size()<<" ";
for(auto v:bl[i]) cout<<v<<" ";cout<<"\n";
}
return 0;
}
//注意有重边
P4652 [CEOI 2017] One-Way Streets
#include<bits/stdc++.h>
#define Pair pair<int,int>
#define v first
#define id second
using namespace std;
const int N=1e5+10;int n;
struct Edge{
int u,v,as;
}edg[N];
vector<Pair> mp[N];
int bcc[N],dfn[N],low[N],cnt;int idx;
vector<int> st;
void Tarjan(int u,int fae){
dfn[u]=low[u]=++cnt;
st.push_back(u);
for(auto e:mp[u]){
int v=e.v;
if(e.id==fae) continue;
if(!dfn[v]){
Tarjan(v,e.id);
low[u]=min(low[u],low[v]);
}
else low[u]=min(low[u],dfn[v]);
}
if(low[u]==dfn[u]){
idx++;
while(1){
int x=st.back();st.pop_back();
bcc[x]=idx;
if(x==u) break;
}
}
}
vector<Pair> sq[N];
void BCC(){
for(int i=1;i<=n;i++){
if(dfn[i]==0) Tarjan(i,0);
}
for(int u=1;u<=n;u++){
for(auto e:mp[u]){
int v=e.v;
if(bcc[u]==bcc[v]){
edg[e.id].as=-1;
continue;
}
sq[bcc[u]].push_back({bcc[v],e.id});
}
}
}
int sum[N];
int vis[N];
void dfs(int u,int fa){
if(vis[u]) return ;
vis[u]=1;
for(auto e:sq[u]){
int v=e.v;
if(v==fa) continue;
dfs(v,u);
sum[u]+=sum[v];
if(sum[v]==0) edg[e.id].as=-1;
else if(sum[v]>0) edg[e.id].as=u;
else edg[e.id].as=v;
}
}
int main(){
int m;cin>>n>>m;
for(int i=1;i<=m;i++){
int a,b;cin>>a>>b;
edg[i]={a,b,0};
mp[a].push_back({b,i});
mp[b].push_back({a,i});
}
BCC();
for(int i=1;i<=m;i++){
edg[i].u=bcc[edg[i].u];
edg[i].v=bcc[edg[i].v];
}
int q;cin>>q;
while(q--){
int x,y;cin>>x>>y;
sum[bcc[x]]++;sum[bcc[y]]--;
}
for(int i=1;i<=idx;i++){
if(vis[i]) continue;
dfs(i,0);
}
for(int i=1;i<=m;i++){
int u=edg[i].u,v=edg[i].v,as=edg[i].as;
if(as==-1) cout<<"B";
else if(as==u) cout<<"L";
else cout<<"R";
}
return 0;
}
//搜边双,边双可以按照SCC的方式建边,共两种,所以边双内的边都是B
//然后边双缩点
//x->y x处标记++ y处--
//然后统计子树和,判断正负号
三:【强连通分量】
若\(dfn==low\),则反复弹栈,直到x被弹出
P2863 [USACO06JAN] The Cow Prom S
#include<bits/stdc++.h>
using namespace std;
const int N=1e4+10;
vector<int> mp[N];
int n;
int dfn[N],low[N],cnt;
int scc[N];
vector<int> st;int idx;
void Tarjan(int u){
dfn[u]=low[u]=++cnt;
st.push_back(u);
for(auto v:mp[u]){
if(!dfn[v]){
Tarjan(v);
low[u]=min(low[u],low[v]);
}
else if(!scc[v]) low[u]=min(low[u],dfn[v]);
}
if(low[u]==dfn[u]){
idx++;
while(1){
int x=st.back();st.pop_back();
scc[x]=idx;
if(x==u) break;
}
}
}
int siz[N];
void SCC(){
for(int i=1;i<=n;i++){
if(dfn[i]==0) Tarjan(i);
}
for(int i=1;i<=n;i++) siz[scc[i]]++;
}
int main(){
int m;cin>>n>>m;
while(m--){
int a,b;cin>>a>>b;
mp[a].push_back(b);
}
SCC();
int ans=0;
for(int i=1;i<=idx;i++) ans+=(siz[i]>1);
cout<<ans<<"\n";
return 0;
}
四:【性质】
以上连通分量的性质可以直接由定义出发,不过还有一个性质,即为同连通分量内任意两个点都可以同属一个回路
然后可以基于这些性质进行推论,不做展开
# 伍:【圆方树】
其实并不是什么高端的东西,类似于强连通分量和边双缩点
由于割点属于至少两个点双,所以缩点就不一定有良好性质
我们换个想法,建立圆方树
一:【构造】
一个点双连通分量生成一个新的节点,我们称之为方点,而原图中的点我们称之为圆点
将方点和它对应的圆点连边
我们就用一张图造出了一颗圆方树,然后可以在方点上统一维护信息
构造过程如图示



二:【性质】
- 相邻点形状不同
- 度数\(>1\)的圆点,在原图中是割点
- 方点度数是点双大小
CF487E Tourists
#include<bits/stdc++.h>
#define inf 1e9+10
using namespace std;
const int N=2e5+10;int n;
vector<int> mp[N];
int nw[N];
int dfn[N],low[N],cnt;
vector<int> st;int idx;
vector<int> bl[N];
void Tarjan(int u,int fa){
dfn[u]=low[u]=++cnt;
st.push_back(u);
for(auto v:mp[u]){
if(v==fa) continue;
if(!dfn[v]){
Tarjan(v,u);
low[u]=min(low[u],low[v]);
if(low[v]>=dfn[u]){
idx++;
while(1){
int x=st.back();st.pop_back();
bl[idx].push_back(x);
if(x==v) break;
}bl[idx].push_back(u);
}
}
else low[u]=min(low[u],dfn[v]);
}
}
vector<int> sq[N];
multiset<int> xl[N];
void BCC(){
for(int i=1;i<=n;i++){
if(dfn[i]==0) Tarjan(i,0);
}
for(int u=n+1;u<=idx;u++){
for(auto v:bl[u]){
sq[u].push_back(v);
sq[v].push_back(u);
}
}
}
int fa[N],son[N],siz[N],dep[N];
void Son(int u,int pa){
fa[u]=pa;
siz[u]=1;
dep[u]=dep[pa]+1;
for(auto v:sq[u]){
if(v==pa) continue;
if(u>n) xl[u].insert(nw[v]);
Son(v,u);
siz[u]+=siz[v];
if(siz[v]>siz[son[u]]) son[u]=v;
}
}
int top[N],tot;//dfn[N]
void Line(int u,int tp){
dfn[u]=++tot;
top[u]=tp;
if(!son[u]) return ;
Line(son[u],tp);
for(auto v:sq[u]){
if(v==son[u]||v==fa[u]) continue;
Line(v,v);
}
}
int tree[N<<2];
void push_up(int q){
tree[q]=min(tree[q<<1],tree[q<<1|1]);
}
void update(int q,int l,int r,int tp,int d){
if(l==r){
tree[q]=d;
return ;
}
int mid=l+r>>1;
if(tp<=mid) update(q<<1,l,mid,tp,d);
else update(q<<1|1,mid+1,r,tp,d);
push_up(q);
}
int query(int q,int l,int r,int L,int R){
if(L<=l&&r<=R){
return tree[q];
}
int ans=inf;
int mid=l+r>>1;
if(L<=mid) ans=min(ans,query(q<<1,l,mid,L,R));
if(mid<R) ans=min(ans,query(q<<1|1,mid+1,r,L,R));
return ans;
}
int solve(int a,int b){
int ans=inf;
while(top[a]!=top[b]){
if(dep[top[a]]<dep[top[b]]) swap(a,b);
ans=min(ans,query(1,1,idx,dfn[top[a]],dfn[a]));
a=fa[top[a]];
}
if(dep[a]>dep[b]) swap(a,b);
ans=min(ans,query(1,1,idx,dfn[a],dfn[b]));
if(a>n) ans=min(ans,nw[fa[a]]);
return ans;
}
int main(){
ios::sync_with_stdio(0);cin.tie(0);cout.tie(0);
int m,q;cin>>n>>m>>q;
idx=n;
for(int i=0;i<N;i++) nw[i]=inf;
for(int i=1;i<=n;i++) cin>>nw[i];
while(m--){
int a,b;cin>>a>>b;
mp[a].push_back(b);
mp[b].push_back(a);
}
BCC();
Son(1,0);
Line(1,1);
for(int i=0;i<(N<<2);i++) tree[i]=inf;
for(int i=1;i<=n;i++) update(1,1,idx,dfn[i],nw[i]);
for(int i=n+1;i<=idx;i++) update(1,1,idx,dfn[i],*xl[i].begin());
while(q--){
char op;cin>>op;
int a,b;cin>>a>>b;
if(op=='C'){
if(fa[a]){
xl[fa[a]].erase(xl[fa[a]].find(nw[a]));
xl[fa[a]].insert(b);
update(1,1,idx,dfn[fa[a]],*xl[fa[a]].begin());
}
nw[a]=b;
update(1,1,idx,dfn[a],nw[a]);
}
else{
cout<<solve(a,b)<<"\n";
}
}
return 0;
}
//建圆方树,方点用multiset维护周围圆点最小值
//修改时用圆点更新周围方点multiset
//然后树链剖分加速询问
//但是如果出现菊花图,原点周围可能出现非常非常多的方点,会被卡掉
//这里有一个Trick,只维护修改父节点位置的方点,其他位置不管,在solve函数的最后一步取min即可
//这里进行一个拓展:如果把一个点不能被经过多次 改成 一条边不能经过多次该怎么做
//我的想法是,边双缩点,树剖加速,实际上更简单了
# 陆:【Boruvka】
一:【实现】
即为多路合并\(Prim\)
每次遍历所有连通块,对于每个连通块,求其与其他连通块连边的权值最小值,进行合并
如果以边来遍历,复杂度\(O(mlogn)\)
以点来遍历,复杂度一般为\(O(nlog^2n)\),因为一般需要数据结构加速
二:【应用】
这个算法稠密图没Prim效率高,稀疏图没Kruskal效率高
但在特殊问题下是杀招
这类特殊条件形如 给你一个完全图,完全图上的边权可以通过端点的点权经过某种计算得出,求最小生成树
这类问题,\(kruskal-O(n^2 log n^2),Prim-O(n^4)\),都解决不了
但是如果用\(Boruvka\)的话,以点来遍历,数据结构(比如线段树)维护,复杂度 \(O(nlog^2n)\)
P3366 【模板】最小生成树
#include<bits/stdc++.h>
#define Pair pair<int,int>
#define w first
#define v second
#define inf 2e9+10;
using namespace std;
const int N=5010;
vector<Pair> mp[N];
int lk[N],mn[N];
int fa[N];
int find(int x){
if(x==fa[x]) return x;
return fa[x]=find(fa[x]);
}
int main(){
int n,m;cin>>n>>m;
while(m--){
int a,b,c;cin>>a>>b>>c;
mp[a].push_back({c,b});
mp[b].push_back({c,a});
}
for(int i=1;i<=n;i++) fa[i]=i;
int ans=0;
while(1){
for(int i=1;i<=n;i++){
lk[i]=-1;
mn[i]=inf;
}
for(int u=1;u<=n;u++){
int x=find(u);
for(auto e:mp[u]){
int y=find(e.v),w=e.w;
if(x==y) continue;
if(w<mn[x]){
lk[x]=y;mn[x]=w;
}
}
}
int flag=1;
for(int u=1;u<=n;u++){
if(lk[u]==-1) continue;
int x=find(u),y=find(lk[u]);
if(x==y) continue;
fa[x]=y;
ans+=mn[x];
flag=0;
}
if(flag) break;
}
set<int> q;
for(int i=1;i<=n;i++) q.insert(find(i));
if(q.size()>1) cout<<"orz\n";
else cout<<ans<<"\n";
return 0;
}
CF888G
#include<bits/stdc++.h>
#define inf 2e9+10
#define Pair pair<int,int>
#define w first
#define v second
using namespace std;
typedef long long LL;
const int N=2e5+10,S=N*70;
int ak[S][2],cnt,siz[S];int tail[S];
int root[N];
int a[N];
void insert(int &rt,int x,int tp){
if(!rt) rt=++cnt;
int now=rt;
siz[now]++;
for(int i=30;i>=0;i--){
int k=x>>i&1;
if(ak[now][k]==0) ak[now][k]=++cnt;
now=ak[now][k];
siz[now]++;
}
tail[now]=tp;
}
int merge(int a,int b){
if(!a||!b) return a+b;
ak[a][0]=merge(ak[a][0],ak[b][0]);
ak[a][1]=merge(ak[a][1],ak[b][1]);
if(ak[a][0]+ak[a][1]==0) siz[a]=1;
else siz[a]=siz[ak[a][0]]+siz[ak[a][1]];
tail[a]=tail[b];
return a;
}
Pair query(int las,int now,int x){
LL ans=0;
for(int i=30;i>=0;i--){
int k=x>>i&1;
if(siz[ak[now][k]]-siz[ak[las][k]]>0){
now=ak[now][k];
las=ak[las][k];
}
else{
ans+=1ll<<i;
now=ak[now][!k];
las=ak[las][!k];
}
}
return {ans,tail[now]};
}
int lk[N],mn[N];
int fa[N];
int find(int x){
if(x==fa[x]) return x;
return fa[x]=find(fa[x]);
}
signed main(){
ios::sync_with_stdio(0);cin.tie(0);cout.tie(0);
int n;cin>>n;
for(int i=1;i<=n;i++){
cin>>a[i];
}
sort(a+1,a+1+n);n=unique(a+1,a+1+n)-a-1;
for(int i=1;i<=n;i++){
insert(root[0],a[i],i);
insert(root[i],a[i],i);
}
LL ans=0;
for(int i=1;i<=n;i++) fa[i]=i;
while(1){
for(int i=1;i<=n;i++){
lk[i]=-1;
mn[i]=inf;
}
for(int u=1;u<=n;u++){
int x=find(u);
Pair tmp=query(root[x],root[0],a[u]);
int w=tmp.w,y=find(tmp.v);
if(x==y) continue;
if(w<mn[x]){
mn[x]=w;
lk[x]=y;
}
}
bool flag=1;
for(int u=1;u<=n;u++){
int x=find(u);
if(lk[x]==-1) continue;
int y=find(lk[x]),w=mn[x];
if(x==y) continue;
flag=0;
fa[y]=x;
root[x]=merge(root[x],root[y]);
ans+=w;
}
if(flag) break;
}
cout<<ans<<"\n";
return 0;
}
//全局建01trie,每个连通块再建一个01trie,做差就是此连通块外的01trie
//对于一个连通块,暴力记录到其他连通块的边权最小值
//多路合并
# 柒:【kruskal重构树】
一:【构造】
\(kruskal\)执行的过程中,对于一条边\((u,v,w)\)
记\(u\)在重构树上的根节点为\(u',v\)为\(v'\)
新建一个节点\(x,x\)的左右儿子分别为\(u',v',x\)的权值为\(w\)
\(kruskal\)跑完之后,\(kruskal\)重构树构造完成
二:【性质】
- 大根堆
- 两点简单路径最大边权的最小值为 它们在重构树上的\(LCA\)的权值
- \(u\)走权值\(<=k\)的边可达的点集一定是 \(kruskal\)重构树上某个包含\(u\)的子树
三:【应用】
- 性质二:求某两个点简单路径最大边权最小值
- 性质三:求\(u\)走权值\(<=k\)的边可达的点集
P1967 [NOIP 2013 提高组] 货车运输
#include<bits/stdc++.h>
using namespace std;
const int N=5e4+10;
struct Edge{
int u,v,w;
bool operator<(Edge b){
return w>b.w;
}
}edg[N];
int bcj[N];
int nw[N];
vector<int> mp[N];int idx;
int find(int x){
if(x==bcj[x]) return x;
return bcj[x]=find(bcj[x]);
}
int ind[N];
int dep[N],siz[N],son[N],fa[N],top[N];
void Son(int u,int pa){
fa[u]=pa;
dep[u]=dep[pa]+1;
siz[u]=1;
for(auto v:mp[u]){
if(v==pa) continue;
Son(v,u);
siz[u]+=siz[v];
if(siz[v]>siz[son[u]]) son[u]=v;
}
}
void Line(int u,int tp){
top[u]=tp;
if(!son[u]) return ;
Line(son[u],tp);
for(auto v:mp[u]){
if(v==son[u]||v==fa[u]) continue;
Line(v,v);
}
}
int LCA(int a,int b){
while(top[a]!=top[b]){
if(dep[top[a]]<dep[top[b]]) swap(a,b);
a=fa[top[a]];
}
if(dep[a]>dep[b]) swap(a,b);
return a;
}
int main(){
int n,m;cin>>n>>m;idx=n;
for(int i=1;i<=m;i++){
int u,v,w;cin>>u>>v>>w;
edg[i]={u,v,w};
}
sort(edg+1,edg+1+m);
for(int i=1;i<=2*n;i++) bcj[i]=i;
for(int i=1;i<=m;i++){
int u=edg[i].u,v=edg[i].v,w=edg[i].w;
u=find(u);v=find(v);
if(u==v) continue;
nw[++idx]=w;
bcj[u]=bcj[v]=idx;
mp[idx].push_back(u);ind[u]++;
mp[idx].push_back(v);ind[v]++;
}
for(int i=1;i<=idx;i++){
if(ind[i]!=0) continue;
Son(i,0);
Line(i,i);
}
int q;cin>>q;
while(q--){
int a,b;cin>>a>>b;
if(find(a)!=find(b)) cout<<-1<<"\n";
else cout<<nw[LCA(a,b)]<<"\n";
}
return 0;
}
# 捌:【Kosaraju】(施工中)
一个求\(SCC\)的算法
有些题目可以采取\(bitset\)优化,这些是\(tarjan\)做不了的
比如\(HDU6072\)
这里不做展开 我懒得学以后再补
# 玖:【次小生成树】(施工中)
# 拾:【拟阵】(施工中)
学不懂(晕)

浙公网安备 33010602011771号