【賽後總結】妙不可言的圖論
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;
}