1. 程式人生 > 其它 >【賽後總結】妙不可言的圖論

【賽後總結】妙不可言的圖論

T1:消防

Solve

好了,其實我不會,再見

Code

T2:Network

一開始以為這是二分……

這道題和貨車運輸很像,只是一個是求最短邊的最大值,一個是求最長邊的最小值。

方法1:最小生成樹+LCA

通過觀察樣例或者手玩一些資料不難發現,一些邊值較大的數根本不會被考慮到。在保證連通性和刪掉儘可能多的長邊的前提下,我們發現它具有最小生成樹的特性。

由於是在樹上,兩點之間的路徑是確定的,我們只需求出這條路徑即可查詢它的最長邊。可以嘗試樸素超時演算法,也可以借用求 LCA 的思路進行倍增。

定義 \(dis_{[i][j]}\) 表示從 i 節點到它的第 \(2^j\) 個父節點路徑上邊的最大值,與 LCA 維護方式相似。

整體思路

以樣例為例,如下圖

現在進行最小生成樹操作

不難發現,兩個節點在樹上的路徑一定要經過他們的最近公共祖先

如圖上,從 1 到 4 的路徑上的最大值要麼在 1-3 這條鏈上,要麼在 3-4 這條鏈上。

按照 LCA 的思路在樹上跳來跳去順便維護一下最大值就行了

程式碼

#include<bits/stdc++.h>
using namespace std;

const int N=3e4+5;
int n,m;
int head[N<<1],tot;
int cnt;
int fa[N],tfa[N][30];
int deep[N],dis[N][30];
int ansi,ln;
bool vis[N];

struct node{
	int x,y,w;
}mapi[N];

struct Node{
	int nex,to,w;
}edge[N<<1];

bool cmp(node x,node y){
	return x.w<y.w;
}

void add(int x,int y,int z){
	edge[++tot].to=y;
	edge[tot].nex=head[x];
	edge[tot].w=z;
	head[x]=tot;
}
//並查集基礎操作 
int find(int x){
	if(fa[x]==x) return x;
	return fa[x]=find(fa[x]);
}

void unionn(int x,int y){
	int fx=find(x),fy=find(y);
	if(fx!=fy) fa[fx]=fy;
}

void kruskal(){
	for(int i=1;i<=n;i++) fa[i]=i;//並查集初始化 
	sort(mapi+1,mapi+m+1,cmp);//從小到大排序(貪心思想) 
	for(int i=1;i<=m;i++){
		int x=mapi[i].x,y=mapi[i].y;
		if(find(x)!=find(y)){//如果之前不連通 
			unionn(x,y);
			cnt++;
			add(x,y,mapi[i].w);add(y,x,mapi[i].w);
		}
		if(cnt==n-1) return;//已經是樹了 
	}
}

void dfs(int x,int fx){
	deep[x]=deep[fx]+1;//深度為它的父節點深度加 1 
	tfa[x][0]=fx;
	vis[x]=true;
	for(int i=1;i<=ln;i++){
		tfa[x][i]=tfa[tfa[x][i-1]][i-1];//LCA核心思想:x 的第2^j個祖先就是它的2^(j-1)個祖先的第2^(j-1)個祖先 
		dis[x][i]=max(dis[x][i-1],dis[tfa[x][i-1]][i-1]);//最大邊權 
	}
	for(int i=head[x];i;i=edge[i].nex){
		int nex=edge[i].to;
		if(vis[nex]==true) continue;
		dis[nex][0]=edge[i].w;//到自己父親的最大邊權就是連邊 
		dfs(nex,x);
	}
}

int lca(int x,int y){
	if(deep[x]>deep[y]) swap(x,y);//保證是 y 跳 
	for(int i=ln;i>=0;i--){
		if((deep[y]-deep[x])>>i&1!=0){//可以跳 
			ansi=max(ansi,dis[y][i]);
			y=tfa[y][i];//跳啊跳 
		}
	}
	if(x==y) return ansi;//找到最近公共祖先,也就是 x 
	for(int i=ln;i>=0;i--){
		if(tfa[x][i]!=tfa[y][i]){//只要父親還不相同 
			ansi=max(ansi,max(dis[x][i],dis[y][i]));
			x=tfa[x][i];y=tfa[y][i];//跳啊跳 
		}
	}
	return max(ansi,max(dis[x][0],dis[y][0]));//與最近公共祖先的連邊也不要忘了取 max 
}

int main(){
	ios::sync_with_stdio(false);
	int q;
	cin>>n>>m>>q;
	ln=log2(n);
	for(int i=1;i<=m;i++) cin>>mapi[i].x>>mapi[i].y>>mapi[i].w;
	kruskal();//求最小生成樹 
	for(int i=1;i<=n;i++){//預處理深度和父節點 
		if(vis[i]==true) continue;//如果之前就預處理過了 
		dfs(i,0);//預處理 
	}
	while(q--){
		int x,y;
		cin>>x>>y;
		ansi=0;//最大邊邊權 
		cout<<lca(x,y)<<"\n"; 
	}
}

Kruskal 重構樹

Kruskal 重構樹的模板題。

Kruskal 重構樹的性質:求最值的最值,剛好和這道題最長邊的最小值專業對口,於是我們考慮用它來解決

可參照這位神犇的部落格(侵權衫):Kruskal 重構樹部落格

整體思路

首先用 Kruskal 重構樹預處理出所有路徑最長邊的最小值,然後 LCA 即可

程式碼

咕咕咕

T3:JOIOJI

以為是 dp……

觀察題面中的J、O、I三個字母的出現次數恰好相同,可以考慮用字首和維護它們的出現次數。假設當前在第 i 個位置,進行分類討論。

對於數列 1-i,可以直接查詢幾個字母出現的次數是否相同。

對於其他數列,我們不難得出以下式子:

(陣列定義:\(suma_{[i]}\)
為 J 的字首和,\(sumb_{[i]}\)為 的字首和,\(sumc_{[i]}\)為 I 的字首和,j 為數列左邊界,i 為數列右邊界)
\[suma_{[j]}-suma_{[i-1]} = sumb_{[j]}-sumb_{[i-1]} \] \[sumb_{[j]}-sumb_{[i-1]} = sumc_{[j]}-sumc_{[i-1]} \]

移項得:

\[suma_{[j]}-sumb_{[j]} = suma_{[i-1]}-sumb_{[i-1]} \] \[sumb_{[j]}-sumc_{[j]} = sumb_{[i-1]}-sumc_{[i-1]} \]

也就是說,當一個數列滿足條件時,它左邊界字首和陣列相減的差要等於右邊界字首和陣列相減的差。

這裡用到了一點點的貪心思想,要想數列最長,左邊界越靠前越好。用一個數組儲存兩個式子的值,之後再出現這兩個式子的值時,就將數列長度取最大值就好。

整體思想

儲存字首和,分類討論兩種情況。注意陣列要開動態的,不然會爆。

程式碼

#include<bits/stdc++.h>
using namespace std;

const int N=2e5+5;
int n,suma[N],sumb[N],sumc[N];
map<int,map<int,int> >tim;

int main(){
	ios::sync_with_stdio(false);
	cin>>n;
	for(int i=1;i<=n;i++){
		char c;
		cin>>c;
		suma[i]=suma[i-1];sumb[i]=sumb[i-1];sumc[i]=sumc[i-1];
		if(c=='J') suma[i]++;
		else if(c=='O') sumb[i]++;
		else sumc[i]++;
	}
	int ansi=0;
	for(int i=1;i<=n;i++){
		if(suma[i]==sumb[i]&&sumb[i]==sumc[i]) ansi=max(ansi,i);
		int x=suma[i]-sumb[i],y=sumb[i]-sumc[i];
		if(tim[x][y]==0) tim[x][y]=i;
		ansi=max(ansi,i-tim[x][y]);
	}
	cout<<ansi<<"\n";
	return 0;
}