集訓D4-5

wertyuio1發表於2024-08-18

DAT4-5 圖論

最短路

性質

\(dis[u]\)代表從源點走到u的最短路長度

1.貪心性:源點到任意一個點最短路上的每一步都是一個最短路

2.存在性:兩個點之間的最短路有可能不存在。(源點存在一個到達該點且經過一個負環的路徑/圖不連通)

3.三角形不等式:對於一條邊\(u\stackrel{w}{\to}v\),一定有\(dis[v]<=dis[u]+w\)

4.最短路圖:從源點出發,把所有\(dis[v]=dis[u]+w\)的邊建出來可以得到一張DAG。(不考慮0環)

演算法

鬆弛操作:對於一條邊,若當前\(dis[v]>dis[u]+w\),則置\(dis[v]=dis[u]+w\)

1.Bellman-Ford(SPFA)

\(O(nm)\)

2.Dijkstra

樸素\(O(n^2)\)

堆最佳化\(O((n+m)log(n+m))\)

只用於正權圖

3.Floyd

\(O(n^3)\)

4.Johnson

用Dijkstra跑全源最短路

解決負權邊:增加點權\(h[u]\),將邊的權值由\(w\)改為\(w+h[u]-h[v]\)

這樣不會影響最短路大小比較

\(h[u]\)滿足性質:\(w+h[u]-h[v]>=0\),即\(h[v]<=w+h[u]\)

建立一個超級源點(防止有點遍歷不到)跑Bellman-Ford,得到\(h[u]\)

\(O(nmlogm)\)

bool check(){
	for(int i=1;i<=n;i++)h[i]=1e9;
	queue<int>q;
	q.push(0);
	while(!q.empty()){
		int u=q.front();
		q.pop();
		for(int j=0;j<a[u].size();j++){
			int v=a[u][j].v,w=a[u][j].w;
			if(h[v]>h[u]+w){
				h[v]=h[u]+w;
				q.push(v);
				in[v]++;
				if(in[v]>n+1)return 1;//判斷負環,加上超級源點共n+1個點
			}
		}
	}
	return 0;
}
void dijkstra(int s){
	memset(vis,0,sizeof vis);
	for(int i=1;i<=n;i++)d[i]=1e9;
	d[s]=0;
	priority_queue<node>q;
	q.push(node{s,0});
	while(!q.empty()){
		node now=q.top();
		q.pop();
		if(vis[now.u])continue;
		else vis[now.u]=1;
		for(int j=0;j<a[now.u].size();j++){
			int v=a[now.u][j].v,w=a[now.u][j].w;
			if(d[v]>d[now.u]+w+h[now.u]-h[v]){
				d[v]=d[now.u]+w+h[now.u]-h[v];
				q.push(node{v,d[v]});
			}
		}
	}
}
for(int i=1;i<=n;i++)a[0].push_back(edge{i,0});//建立超級源點
for(int i=1;i<=n;i++)dijkstra(i);//統計答案時特判路徑不存在的情況

問題

1.輸出方案

\(pre[i]\)記錄前驅節點

2.傳遞閉包

bitset最佳化\(O(\frac{n^3}{w})\)

3.選擇\(k\)條道路以\(c\)代價通行,求最短路

分層圖 \(O(kmlogm)\)

for(int i=1;i<=m;i++){//P4568 P4822
    int u=read()+1,v=read()+1,w=read();	
    a[u].push_back(edge{v,w});
    a[v].push_back(edge{u,w});
    for(int j=1;j<=k;j++){
        a[u+j*n].push_back(edge{v+j*n,w});
        a[v+j*n].push_back(edge{u+j*n,w});
        a[u+(j-1)*n].push_back(edge{v+j*n,0});
        a[v+(j-1)*n].push_back(edge{u+j*n,0});
    }
}

4.給出一個\(N\)個頂點\(M\)條邊的無向無權圖,頂點編號為\(1∼N\),求從頂點\(1\)開始,到其他各點的最短路條數

void bfs(){//P1144
...
		for(int v:a[u]){	
			if(d[v]>d[u]+1){
				d[v]=d[u]+1;
				f[v]=f[u];
				q.push(v);
			}else if(d[v]==d[u]+1){
				f[v]=(f[v]+f[u])%p;
			}
		}
...
}

P1462

二分點權,最短路判定

不開longlong見祖宗

P1119

理解Floyd本質

void floyd(){
	init();
	int now=1;
	while(q--){
		int u=read()+1,v=read()+1,tim=read();
		while(t[now]<=tim&&now<=n){
			for(int i=1;i<=n;i++)
				for(int j=1;j<=n;j++)
					f[i][j]=min(f[i][j],f[i][now]+f[now][j]);
			now++;
		}
        ask(u,v);
	}
}

P1772

動態規劃

列舉運輸路線的更換時間\(j\)

\(f[i]=min(f[j-1]+(i-j+1)\times{}dijkstra(j,i)+k)),j\in{}[1,i]\)

dijkstra時忽略不可到達的點

最終結果為\(f[n]-k\)

ll dijkstra(ll tl,ll tr){
	...
		for(int j=0;j<a[now.u].size();j++){
			ll v=a[now.u][j].v,w=a[now.u][j].w;
			if(check(v,tl,tr)){//判斷點是否可行
				if(dist[v]>dist[now.u]+w){
					dist[v]=dist[now.u]+w;
					q.push(node{v,dist[v]});
				}
			}
		}
	...
}
for(int i=1;i<=n;i++){
    for(int j=1;j<=i;j++){
        f[i]=min(f[i],f[j-1]+1ll*(i-j+1)*dijkstra(j,i)+k);

P2371

並查集

void init(){//P3367
	for(int i=1;i<=n;i++)f[i]=i;
}
int find(int i){
	if(f[i]==i)return i;
	else return f[i]=find(f[i]);//路徑壓縮最佳化
}
void merge(int i,int j){//合併
	f[find(i)]=find(j);
}
bool query(int i,int j){//查詢
	return find(i)==find(j);
}

最佳化

1.路徑壓縮

2.按秩(啟發式)合併

void init(){
    for(int i=1;i<=n;i++)f[i]=i,siz[i]=1;
}
void merge(int i,int j){
	i=find(i),j=find(j);
	if(i==j)return;
	if(siz[i]>siz[j])swap(i,j);
	f[i]=j;siz[j]+=siz[i];
}

任意一種最佳化單詞均攤複雜度為\(O(logn)\)

全部使用複雜度為\(O(\alpha(n))\)

問題

1.維護每個集合的點權和/maxn

帶權並查集

2.統計目前集合數量

for(int i=1;i<=n;i++)if(f[i]==i)ans++;

3.將某元素從集合A移動到集合B

修改根,維護資訊

4.利用並查集維護\(n\)個命題,有\(m\)個條件

條件為\(p\Leftrightarrow q\)\(p\Leftrightarrow\neg{}q\)兩種

開正反兩個並查集

注意:必要的時候捨棄“路徑壓縮”操作。因為它破壞了並查集的樹結構。同時會造成一些複雜度錯誤。

P2024

//正反集
int f[150004];// 同類 敵人 食物 
		
if(x>n||y>n||(x==y&&opt==2)){
    ans++;
    continue;
}
if(opt==1){
    if(find(x+n)==find(y)||find(x+2*n)==find(y)){
        ans++;
    }else{
        merge(x,y);
        merge(x+n,y+n);
        merge(x+2*n,y+2*n);
    }
}else{
    if(find(x)==find(y)||find(x+n)==find(y)||find(y+2*n)==find(x)){
        ans++;
    }else{
        merge(x+2*n,y);
        merge(x+n,y+2*n);
        merge(x,y+n);
    }
}
//帶權並查集
int find(int i){
	if(f[i]!=i){
		int tmp=f[i];
		f[i]=find(f[i]);
		d[i]=(d[i]+d[tmp])%3;
		return f[i];
	}else return i;
}

if(w==1)w=0;
if(u==v&&w==2){
    ans++;
}else if(u>n||v>n){
    ans++;
}else{
    int fu=find(u),fv=find(v);
    if(fu!=fv){
        f[fu]=fv;
        d[fu]=(w+d[v]-d[u]+3)%3;
    }else if((d[u]-d[v]+3)%3!=w){
        ans++;
    }
}

生成樹

最小生成樹:邊權和最小的生成樹。

瓶頸生成樹:最大邊權最小的生成樹。

最小瓶頸路:兩個點之間最大邊權最小的簡單路徑。

性質:

最小生成樹是瓶頸生成樹的充分不必要條件,即一棵最小生成樹一定是一棵瓶頸生成樹,一棵瓶頸生成樹不一定是一棵最小生成樹。

最小生成樹上兩個點的路徑一定是一個最小瓶頸路。

演算法

1.Kruskal

\(O(mlogm)\)

2.Prim

\(O(mlogm)\)

問題

1.思考Kruskal的演算法原理,求結點\(1\)到結點\(n\)的最小瓶頸路的權值

節點\(1\)所在集合與節點\(n\)所在集合合併時,kruskal所選的邊權\(w\)即為結點\(1\)到結點\(n\)的最小瓶頸路的權值

2.並查集與Kruskal的演算法關係密切。在不使用路徑壓縮的情況下,整張圖的並查集會呈現一棵樹的樣貌。這棵樹和最小生成樹有什麼關係

瓶頸路

3.比較Prim和Dijkstra演算法的相似性。指出為何Prim可以處理負權邊和Dijkstra不行

4.獲得 “嚴格次小生成樹”

void dfs(int i,int fa){//P4180
	d[i]=d[fa]+1;
	for(int j=1;(1<<j)<=d[i];j++){
		dp[i][j]=dp[dp[i][j-1]][j-1];
		g[i][j]=max(g[dp[i][j-1]][j-1],g[i][j-1]);
        //維護次大值
		h[i][j]=max(h[dp[i][j-1]][j-1],h[i][j-1]);
		if(g[dp[i][j-1]][j-1]!=g[i][j-1]){
			h[i][j]=max(h[i][j],min(g[dp[i][j-1]][j-1],g[i][j-1]));
		}
	}
	for(int j=0;j<a[i].size();j++){
		int v=a[i][j].v,w=a[i][j].w;
		if(v!=fa){
			dp[v][0]=i;
			g[v][0]=w;
			dfs(v,i);
		}
	}
}
int ask(int i,int j,int lim){
	int ret=0;
	if(d[i]<d[j])swap(i,j);
	for(int k=dis;k>=0;k--){
		if(d[i]-(1<<k)>=d[j]){
			if(g[i][k]==lim)ret=max(ret,h[i][k]);
			else ret=max(ret,g[i][k]);
			i=dp[i][k];
		}
	}
	if(i==j)return ret;
	for(int k=dis;k>=0;k--){
		if(dp[i][k]!=dp[j][k]){
			if(g[i][k]==lim)ret=max(ret,max(h[i][k],h[j][k]));
			else ret=max(ret,max(g[i][k],g[j][k]));
			i=dp[i][k];
			j=dp[j][k];
		}
	}
	if(g[i][0]!=lim)ret=max(ret,g[i][0]);
	if(g[j][0]!=lim)ret=max(ret,g[j][0]);
	return ret;
}
void solve(){
	ans=inf;
	for(int i=1;i<=m;i++){
		if(vis[i]&&e[i].u!=e[i].v){//判斷自環
			int tmp=ask(e[i].u,e[i].v,e[i].w);
			ans=min(ans,sum+e[i].w-tmp);
		}
	}
	cout<<ans;
}
init();
kruskal();//先求最小生成樹
dfs(1,0);//預處理最大值和次大值
solve();//用未選邊替換,更新答案

6.思考最小生成樹構建時的貪心性質,簡要解釋為何最小生成樹不止一個

P1967

求最大生成樹

在生成樹上倍增預處理最小值

lca查詢最小值

void dfs(int i,int fa){
	d[i]=d[fa]+1;
	col[i]=nums;
	for(int j=1;(1<<j)<=d[i];j++){
		dp[i][j]=dp[dp[i][j-1]][j-1];
		maxn[i][j]=min(maxn[dp[i][j-1]][j-1],maxn[i][j-1]);
	}
	for(int j=0;j<a[i].size();j++){
		int v=a[i][j].y,w=a[i][j].z;
		if(v!=fa){
			dp[v][0]=i;
			maxn[v][0]=w;
			dfs(v,i);
		}
	}
}
int ask(int i,int j){
	int ret=0x3f3f3f3f;
	if(d[i]<d[j])swap(i,j);
	for(int k=dis;k>=0;k--){
		if(d[i]-(1<<k)>=d[j]){
			ret=min(ret,maxn[i][k]);
			i=dp[i][k];
		}
	}
	if(i==j)return ret;
	for(int k=dis;k>=0;k--){
		if(dp[i][k]!=dp[j][k]){
			ret=min(ret,min(maxn[i][k],maxn[j][k]));
			i=dp[i][k];
			j=dp[j][k];
		}
	}
	return min(ret,min(maxn[i][0],maxn[j][0]));
}
init();//預處理
kruskal();//求生成樹
for(int i=1;i<=n;i++){
    if(!d[i]){
        nums++;//圖不連通
        dfs(i,0);
    }
}
solve();//查詢

CF1513D

樹上倍增

求lca

void init(int i,int fa){//P3379
	d[i]=d[fa]+1;
	f[i][0]=fa;
	for(int j=1;(1<<j)<=d[i];j++){
		f[i][j]=f[f[i][j-1]][j-1];
	}
	for(int j=0;j<a[i].size();j++){
		int v=a[i][j];
		if(v!=fa){
			init(v,i);
		}
	}
}
int lca(int i,int j){
	if(d[j]>d[i])swap(i,j);
	for(int k=stp;k>=0;k--){//k>=0注意
		if(d[i]-(1<<k)>=d[j]){
			i=f[i][k];
		}
	}
	if(i==j)return i;
	for(int k=stp;k>=0;k--){//k>=0
		if(f[i][k]!=f[j][k]){
			i=f[i][k];
			j=f[j][k];
		}
	}
	return f[i][0];
}
init(s,0);
lca(i,j);

問題

1.求出 k 個點的樹上最近公共祖先

2.將k個點兩兩求樹上最近公共祖先

P4281

暴力去找

pair<int,int> ask(int i,int j){
	int ret=0;
	if(d[i]<d[j])swap(i,j);
	for(int k=dis;k>=0;k--){
		if(d[i]-(1<<k)>=d[j]){
			ret+=dp[i][k];
			i=f[i][k];
		}
	}
	if(i==j)return make_pair(i,ret);
	for(int k=dis;k>=0;k--){
		if(f[i][k]!=f[j][k]){
			ret+=dp[i][k]+dp[j][k];
			i=f[i][k];
			j=f[j][k];
		}
	}
	return make_pair(f[i][0],ret+2);
}
pair<int,int> getp(int A,int B,int C){
	pair<int,int> tmp=ask(B,C);
	pair<int,int> ans=ask(tmp.first,A);
	return make_pair(tmp.first,tmp.second+ans.second);
}
while(m--){
    int x=read(),y=read(),z=read();
    pair<int,int> t1=getp(y,z,x),t2=getp(x,z,y),t3=getp(z,y,x);	
    if(t1.second>t2.second)swap(t1,t2);
    if(t1.second>t3.second)swap(t1,t3);
    cout<<t1.first<<" "<<t1.second<<endl;
}

樹上差分

查詢

1.求樹上路徑經過點的個數

\(=dep[u]+dep[v]-dep[fa[lca]]-dep[lca]\)

2.求樹上路徑的邊權和

\(=dist[u]+dist[v]-2\times{}dist[lca]\)

修改

\(p\rightarrow{}q\)路徑上的每個點點權\(+v\)

\(val[p]=val[p]+v\)

\(val[q]=val[q]+v\)

\(val[lca]=val[lca]-v\)

\(val[f[lca]]=val[f[lca]]-v\)

dfs自下向上查詢

P2680

運輸計劃為樹上一條路徑

使得運輸時間最長的計劃運輸時間最短,二分

對於大於\(mid\)的運輸計劃,找到這些運輸計劃共同經過的最大邊

判斷共同經過使用樹上差分

inline卡常

inline void dfs(int i,int fa){
	for(int j=0;j<a[i].size();j++){
		int v=a[i][j].v,w=a[i][j].w;
		if(v!=fa){
			dfs(v,i);
			val[i]+=val[v];
		}
	}
	for(int j=0;j<a[i].size();j++){//在更新完val[]後再判斷
		int v=a[i][j].v,w=a[i][j].w;
		if(v!=fa){
			if(val[i]==cnt&&val[v]==cnt){//該邊被cnt條路徑經過
				dmax=max(dmax,w);
			}
		}
	}
}
inline bool check(ll x){
	maxn=0;dmax=0;cnt=0;//最長運輸計劃耗時	可減少的最大時間	需要減少時間的邊數
	memset(val,0,sizeof val);//注意
	for(int i=1;i<=m;i++){
		if(q[i].w>x){
			int u=q[i].u,v=q[i].v;
			maxn=max(maxn,q[i].w);cnt++;
			int Lca=getsum(u,v).first;
			//樹上差分
            val[u]++;
			val[v]++;
			val[Lca]--;
			val[f[Lca][0]]--;
		}
	}
	dfs(1,0);
	return maxn-dmax<=x;
}
ll l=0,r=maxn;
while(l<r){
    ll mid=l+(r-l)/2;
    if(check(mid))r=mid;
    else l=mid+1;
}

hdu6053

有一棵樹,有\(n\)個節點,每個節點都有一個用整數表示的顏色型別,其中節點\(i\)的顏色是\(c_i\)

每兩個不同節點之間的路徑是唯一的,我們定義路徑的值為其中出現的不同顏色的數量

計算所有路徑的值的總和,該樹上總共有\(\frac{n(n-1)}{2}\)條路徑

定義\([P]\)\(P\)為真則為\(1\),否則為\(0\)

即求

\({\textstyle \sum_{i=1}^{\frac{n(n-1)}{2}}}{\textstyle\sum_{j=1}^{colnums}[c_j出現次數>=1]}\)

\(={\textstyle \sum_{j=1}^{colnums}{\textstyle\sum_{i=1}^{\frac{n(n-1)}{2}}}[c_j出現次數>=1]}\)

\(=colnums\times\frac{n(n-1)}{2}-{\textstyle \sum_{j=1}^{colnums}{\textstyle\sum_{i=1}^{\frac{n(n-1)}{2}}}[c_j出現次數=0]}\)

// 以1節點為根
ll fi(ll i){//i為連通塊大小,返回連通塊邊數
	return i*(i-1)/2;
}
void dfs(int i,int fa){
	//sum[i]表示當前以i顏色節點為根的子樹大小
    ll inssum=0;//記錄增長
	siz[i]=1;//siz[i]表示以節點i為根的子樹大小
	for(int j=0;j<a[i].size();j++){
		int v=a[i][j];
		if(v!=fa){
			ll pre=sum[c[i]];//記錄過去大小
			dfs(v,i);//遞迴子樹
			siz[i]+=siz[v];//更新
			ll ins=sum[c[i]]-pre;//遞迴子樹之後,sum[c[i]]的增長值,也就是子樹內以i顏色節點為根的子樹大小
			sub+=fi(siz[v]-ins);//子樹大小-ins即為顏色c[i]不出現的連通塊大小
			inssum+=ins;//記錄
		}
	}
	sum[c[i]]+=siz[i]-inssum;//加上以當前節點為根的子樹內的其它節點個數
}
void solve(){
	dfs(1,0);//統計sub,sub為每種顏色未出現邊數之和
	for(int i=1;i<=n;i++)
		cnt+=(csum[i]>0);//統計顏色種類
	//統計每種顏色最淺節點到根之間的貢獻
    vis[c[1]]=1;
	for(int i=1;i<=n;i++){
		if(!vis[c[i]]){
			sub+=fi(n-sum[c[i]]);
			vis[c[i]]=1;
		}
	}
    ans=cnt*fi(n)-sub;
	++cas;
	printf("Case #%lld: %lld\n",cas,ans);
}
Clear();//注意
for()csum[c[i]]++;//統計每種顏色出現次數
solve();

P4768

P1600

P3953

P1084