# 經典題目講解 & 小技巧 ## 圖論 [ABC208D](https://atcoder.jp/contests/abc208/tasks/abc208_d) ``` 這題乍看之下可能很floyd,但可能還是很多人不知道該怎麼做 那我們可以回來想想看floyd的原理是什麼 很簡單,最外層的迴圈代表的其實就是目前可以用到的邊的量 其實只要懂得floyd的原理就很好理解了 ``` ```cpp= #include <bits/stdc++.h> using namespace std; const int maxn = 4e2 + 5 ; const int mod = 1e9 + 7 ; const int INF = 1e9 + 9 ; const double eps = 1e-7 ; int dp[maxn][maxn]; signed main() { int n,m,ans=0; cin>>n>>m; for(int i=1;i<=n;i++) { for(int j=1;j<=n;j++) { dp[i][j]=INF; } } for(int i=1;i<=n;i++) dp[i][i]=0; while(m--) { int u,v,w; cin>>u>>v>>w; dp[u][v]=mixx(w,dp[u][v]); } for(int i=1;i<=n;i++) { for(int j=1;j<=n;j++) { for(int k=1;k<=n;k++) { dp[j][k]=min({dp[j][k],dp[j][i]+dp[i][k]}); ans+=dp[j][k]==INF?0:dp[j][k]; } } } cout<<ans; } ``` [TIOJ 1340](https://tioj.ck.tp.edu.tw/problems/1340) ``` 這題乍看之下沒有什麼頭緒,不過我們很快就可以發現數字移動的範圍不大 我們嘗試把可轉移的數字建一條邊。 那我們要如何處理題目的要求 -- 最小值最大 ? 我們可能會想說可以透過外掛二分搜控制最小值, 並且每次都找一次最大生成樹,樹上的路徑就是答案 但這樣太麻煩了,不僅要重寫一次最大生成樹,複雜度也會多一個logn。 有一個性質是:我們知道最大生成樹 == 最大瓶頸生成樹。 什麼意思呢 ? 就是兩個點要找最大邊的話,那就找最大瓶頸生成樹即可。 要怎麼處理樹上兩點的最大邊 ? 只要利用 LCA 即可 ``` :::spoiler Code ```cpp= #include <bits/stdc++.h> #define pb push_back #define pii pair<int,int> #define tup tuple<int,int,int> #define g0 get<0>(i) #define g1 get<1>(i) #define g2 get<2>(i) #define f first #define s second #define lowbit(x) (x&-x) #define endl '\n' using namespace std; const int maxn = 1e5 + 5 ; vector< tup >edge; vector< pii >adj[maxn]; int pa[maxn],siz[maxn],depth[maxn]; int up[maxn][20],minn[maxn][20]; int n,q; inline int findset(int x){return pa[x]==x?x:pa[x]=findset(pa[x]);} bool cmp(tup i1,tup i2){return get<2>(i1)>get<2>(i2);} void init() { for(int i=1;i<=n;i++) for(int j=i;j<=n;j+=i) { if(j-i>0) edge.pb({j,j-i,i}); if(j+i<=n) edge.pb({j,j+i,i}); } } void Dfs(int x,int p,int level,int value) { depth[x]=++level; up[x][1]=p; minn[x][1]=value; for(auto i:adj[x]) if(i.f!=p) Dfs(i.f,x,level,i.s); } void solve() { for(int i=1;i<=n;i++) pa[i]=i,siz[i]=1; sort(edge.begin(),edge.end(),cmp); for(auto i:edge) { int u=findset(g0),v=findset(g1),w=g2; if(u==v) continue; if(siz[u]>siz[v]) { pa[v]=u; siz[u]+=siz[v]; adj[g0].pb({g1,w}); adj[g1].pb({g0,w}); } else { pa[u]=v; siz[v]+=siz[u]; adj[g0].pb({g1,w}); adj[g1].pb({g0,w}); } } } void Doubled() { for(int c=2;c<=17;c++) for(int b=1;b<=n;b++) { up[b][c]=up[up[b][c-1]][c-1]; minn[b][c]=min(minn[b][c-1],minn[up[b][c-1]][c-1]); } } int print(int u,int v) { if(depth[u]>depth[v]) swap(u,v); int l=depth[v]-depth[u],c=1,lowest=1e18; while(l) { if(l&1) lowest=min(lowest,minn[v][c]),v=up[v][c]; l>>=1; c++; } if(u==v) return lowest; for(int b=19;b>0;b--) { if(!up[u][b]||up[u][b]==up[v][b]) continue; lowest=min({lowest,minn[v][b],minn[u][b]}); u=up[u][b]; v=up[v][b]; } if(u!=v) lowest=min({lowest,minn[v][1],minn[u][1]}); return lowest; } signed main() { ios::sync_with_stdio(0); cin.tie(0); cin>>n>>q; init(); solve(); Dfs(1,0,0,-1); Doubled(); while(q--) { int a,b; cin>>a>>b; cout<<print(a,b)<<endl; } } ``` ::: http://mdcpp.mingdao.edu.tw/contest/24/problem/C ``` 這題應該不難了解到說倒著做會比較好。 是要怎麼倒著做呢 ? 簡單來說,我們會發現若是正著做的話,我們會需要面臨到刪除許多邊的問題。 我們知道一般的並查集是無法完成刪邊的動作的。 那我們會需要換一個方法,我們只要先把會被刪掉的邊先刪掉,那這就會是最後的情況。 我們只需要接下來慢慢一個接著一個把邊接回去,藉由並查集指向點權最大的邊即可。 ``` :::spoiler Code ```cpp= #include <bits/stdc++.h> #define pb push_back #define pii pair<int,int> #define tup tuple<int,int,int> #define g0 get<0>(i) #define g1 get<1>(i) #define g2 get<2>(i) #define f first #define s second #define lowbit(x) (x&-x) #define endl '\n' #define mid ((l+r)>>1) #define md ((l+r)/2) #define i1 (i<<1) #define i2 (i1+1) #define Hash(x,y) (maxn*x+y) #define iofaster ios::sync_with_stdio(0);cin.tie(0) #define fr(a) freopen((a),"r",stdin) #define fw(a) freopen((a),"w",stdout) #define maxx(a,b) (a>b?a:b) #define mixx(a,b) (a>b?b:a) #define mset(a,b) memset(a,b,sizeof(a)) #define all(x) x.begin(),x.end() #define int long long #define double long double using namespace std; const int maxn = 2e5 + 5 ; const int mod = 1e9 + 7 ; const int INF = 1e9 + 9 ; const double eps = 1e-7 ; set<pii>st; int w[maxn],pa[maxn]; int findset(int x) { return x==pa[x]?x:pa[x]=findset(pa[x]); } void uni(int a,int b) { int ap=findset(a); int bp=findset(b); if(ap==bp) return ; if(w[ap]>w[bp]) { pa[bp]=ap; } else if(w[ap]<w[bp]) { pa[ap]=bp; } else { if(ap<bp) pa[bp]=ap; else pa[ap]=bp; } } signed main() { iofaster; int n,m,q; cin>>n>>m>>q; for(int i=1;i<=n;i++) cin>>w[i]; for(int i=1;i<=n;i++) pa[i]=i; while(m--) { int a,b; cin>>a>>b; st.insert({a,b}); st.insert({b,a}); } vector<tup>query; vector<int>ans; while(q--) { int t,a,b; cin>>t; if(t==0) { cin>>a>>b; st.erase({a,b}); st.erase({b,a}); } else cin>>a; query.pb({t,a,b}); } reverse(all(query)); for(auto [u,v]:st) { uni(u,v); } for(auto [t,a,b]:query) { if(t==0) { uni(a,b); } else { int p=findset(a); if(w[p]==w[a]) ans.pb(0); else ans.pb(p); } } reverse(all(ans)); for(auto i:ans) cout<<i<<endl; } ``` ::: ## 並查集 [ABC214D](https://atcoder.jp/contests/abc214/tasks/abc214_d) ``` 透過觀察可以發現,本題的答案就是一個邊往外延伸出去,遇到比他大的邊就停止,看經過了多少邊。 但我們可能不知道要如何執行上面的動作 ? 沒錯,他很難執行,我們需要換個想法。 提示 1 : 排序 提示 2 : 並查集 我們可以發現,假如我們嘗試先把小的邊連起來,那代表當個連通塊的邊一定會比較小,就可以直接算有幾個點符合了 ! Unbelievable ! ``` :::spoiler Code ```cpp= #include <bits/stdc++.h> #define pb push_back #define pii pair<int,int> #define tup tuple<int,int,int> #define g0 get<0>(i) #define g1 get<1>(i) #define g2 get<2>(i) #define f first #define s second #define lowbit(x) (x&-x) #define endl '\n' #define mid ((l+r)>>1) #define i1 (i<<1) #define i2 (i1+1) #define Hash(x,y) (maxn*x+y) #define iofaster ios::sync_with_stdio(0);cin.tie(0) #define fr(a) freopen((a),"r",stdin) #define fw(a) freopen((a),"w",stdout) #define int long long using namespace std; const int maxn = 2e5 + 5 ; const int mod = 1e9 + 7 ; const int INF = 1e9 + 9 ; int pa[maxn],sz[maxn]; vector<tup>edge; int findset(int x) { return x==pa[x]?x:pa[x]=findset(pa[x]); } signed main() { int n,ans=0; cin>>n; for(int i=0;i<n-1;i++) { int u,v,w; cin>>u>>v>>w; edge.pb({w,u,v}); } sort(edge.begin(),edge.end()); for(int i=1;i<=n;i++) pa[i]=i,sz[i]=1; for(auto [w,u,v]:edge) { u=findset(u),v=findset(v); ans+=w*sz[u]*sz[v]; if(sz[u]>sz[v]) pa[v]=u,sz[u]+=sz[v]; else pa[u]=v,sz[v]+=sz[u]; } cout<<ans; } ``` ::: [ABC228D](https://atcoder.jp/contests/abc228/tasks/abc228_d) ``` 簡單來說,題目就是要求你要透過一個點持續的往右找到 -1 的點就停下來改值。 我們需要一個方法能夠持續往右,但直接搜索太慢了 我們可以把已經有值的點連成一個連通塊, 在回答詢問時,若此點還是 -1,我們就修改此點,將左右兩點若是也已經被修改的話就連起來 反之,若此點不是 -1,我們就可以尋找此點連通塊的最右方,就是我們要的位置 註:這題或許可以用 set 做,我不太確定 ``` :::spoiler Code ```cpp= #include <bits/stdc++.h> #define pb push_back #define pii pair<int,int> #define tup tuple<int,int,int> #define g0 get<0>(i) #define g1 get<1>(i) #define g2 get<2>(i) #define f first #define s second #define lowbit(x) (x&-x) #define endl '\n' #define mid ((l+r)>>1) #define i1 (i<<1) #define i2 (i1+1) #define Hash(x,y) (maxn*x+y) #define iofaster ios::sync_with_stdio(0);cin.tie(0) #define fr(a) freopen((a),"r",stdin) #define fw(a) freopen((a),"w",stdout) #define maxx(a,b) (a>b?a:b) #define mixx(a,b) (a>b?b:a) #define mset(a,b) memset(a,b,sizeof(a)) #define all(x) x.begin(),x.end() #define int long long using namespace std; const int maxn = 2e6 + 5 ; const int mod = 1e9 + 7 ; const int INF = 1e9 + 9 ; int pa[maxn],arr[maxn]; int n=(1<<20),q; int findset(int x) { return x==pa[x]?x:pa[x]=findset(pa[x]); } void uni(int a,int b) { if(a<0) a+=n; if(b<0) b+=n; a%=n,b%=n; if(arr[a]==-1 or arr[b]==-1) return ; a=findset(a),b=findset(b); if(a==b) return ; pa[a]=b; } signed main() { //iofaster; mset(arr,-1); for(int i=0;i<n;i++) pa[i]=i; cin>>q; while(q--) { int t,x; cin>>t>>x; int h=x%n; int nt=findset(h); //cout<<h<<' '<<nt<<endl; if(t==1) { if(nt==h and arr[nt]==-1) { arr[nt]=x; uni(h-1,h); uni(h,h+1); } else { nt++; nt%=n; arr[nt%n]=x; uni(nt-1,nt); uni(nt,nt+1); } } else { cout<<arr[h]<<endl; } } } ``` ::: ## 數學 [TOI 2018 D](https://tioj.ck.tp.edu.tw/problems/2052) [題解](https://hackmd.io/@iceylemon157/rkTeB9fgw#/3/22) :::spoiler Code ```cpp= #include <bits/stdc++.h> using namespace std; int ans=0; int d[100]; int c[2000+10][2000+10]; inline void cal(int md) { int sum=0,x=1; for(int i=0;i<70;i++) { x=(x*c[sum+d[i]][sum])%md; sum+=d[i]; } ans=(ans+x)%md; } inline void init(int md) { for(int i=0;i<=2000;i++) { c[i][0]=1; for(int j=1;j<=i;j++) { c[i][j]=(c[i-1][j-1]+c[i-1][j]); int x=c[i-1][j-1]+c[i-1][j]; c[i][j]=x%md; } } } vector<int>v[1000]; int used[2000+10]; signed main() { ios::sync_with_stdio(0); cin.tie(0); int md; string str; cin>>md>>str; init(md); string st=str; sort(st.begin(),st.end()); for(int i=0;i<st.size();i++) d[st[i]-'A']++,v[st[i]-'A'].push_back(i); for(int p=0;p<str.size();p++) { for(int i=0;i<st.size();i++) { if(used[i] or (i!=0 and st[i]==st[i-1])) continue; if(st[i]!=str[p]) { d[st[i]-'A']--; cal(md); d[st[i]-'A']++; } else break; } used[v[str[p]-'A'].back()]=1; v[str[p]-'A'].pop_back(); d[str[p]-'A']--; } cout<<ans; } ``` ::: ## 線段樹 [CF EDU](https://codeforces.com/edu/course/2/lesson/4/3/practice/contest/274545/problem/C) ``` 這題表面上很難想到,我們可以透過一點分析 我們想要藉由同個數之間的範圍加起來算出答案,可以怎麼做 ? 1. 假如已經兩個都出現但只有尾出現,需算出 0 2. 假如已經兩個都出現且在範圍內,需算出 1 3. 假如只有1個出現,需算出 0 我們可以透過當他還沒出現兩次時,前面的數都還是 0 被觸發的時候,前面是 0 , 後面是 1 就可以發現答案算出來了 ! 詳細請同學自行證明 ``` :::spoiler Code ```cpp= #include <bits/stdc++.h> #define pb push_back #define pii pair<int,int> #define tup tuple<int,int,int> #define g0 get<0>(i) #define g1 get<1>(i) #define g2 get<2>(i) #define f first #define s second #define lowbit(x) (x&-x) #define endl '\n' #define mid ((l+r)>>1) #define i1 (i<<1) #define i2 (i1+1) #define Hash(x,y) (maxn*x+y) #define iofaster ios::sync_with_stdio(0);cin.tie(0) #define fr(a) freopen((a),"r",stdin) #define fw(a) freopen((a),"w",stdout) #define maxx(a,b) (a>b?a:b) #define mixx(a,b) (a>b?b:a) #define mset(a,b) memset(a,b,sizeof(a)) #define all(x) x.begin(),x.end() using namespace std; const int maxn = 2e5 + 5 ; const int mod = 1e9 + 7 ; const int INF = 1e9 + 9 ; int pos[maxn],val[4*maxn],ans[maxn]; void modify(int l,int r,int p,int i) { if(l==r) { val[i]++; return ; } if(p<=mid) modify(l,mid,p,i1); else modify(mid+1,r,p,i2); val[i]=val[i1]+val[i2]; } int query(int l,int r,int L,int R,int i) { if(L<=l and R>=r) { return val[i]; } int sum=0; if(L<=mid) sum+=query(l,mid,L,R,i1); if(R>mid) sum+=query(mid+1,r,L,R,i2); return sum; } signed main() { iofaster; int n; cin>>n; for(int i=0;i<2*n;i++) { int a; cin>>a; if(pos[a]) { modify(1,n*2,pos[a],1); ans[a]=query(1,n*2,pos[a]+1,2*n,1); } else { pos[a]=i+1; } } for(int i=1;i<=n;i++) cout<<ans[i]<<' '; } ``` ::: ## 小技巧 ( 可以自己想想看 ) - 差分 http://mdcpp.mingdao.edu.tw/contest/24/problem/A https://zerojudge.tw/ShowProblem?problemid=g597 - 單調隊列 http://mdcpp.mingdao.edu.tw/contest/24/problem/B