7月清北学堂培训 Day 5
今天是鐘皓曦老師的講授~
動態規劃
動態規劃的三種實現方法:
1.遞推;
2.遞歸;
3.記憶化;
?
舉個例子:
斐波那契數列:0,1,1,2,3,5,8……
Fn = Fn-1 + Fn-2
?
1.我們直接遞推,用別人的結果得到自己的結果:
#include<iostream>using namespace std;int main() {cin >> n;f[0]=0;f[1]=1;for (int a=2;a<=n;a++)f[a] = f[a-1] + f[a-2];cout << f[n];return 0; }?
?
2.用自己的結果去算其他的結果:
#include<iostream>using namespace std;int main() {cin >> n;f[0]=0;f[1]=1;for (int a=0;a<n;a++){f[a+1] += f[a];f[a+2] += f[a];}return 0; }在動態規劃的時候,任何一個題都可以用這兩種方法去寫;
但是不同的題對兩種方法有優有劣,所以我們兩種方法都要會。
?
3.記憶化搜索:
我們很容易發現求斐波那契數列的過程就是遞歸的過程,那么就可以寫一下遞歸的代碼:
由于我們這種方法的斐波那契數是一個一個加上去的,時間復雜度是O(Fn);
如果一個東西已經被算出來了,那么我們就把它存下來,以后直接訪問就好了,不用再遞歸,這就是記憶化搜索:
int f[233]; bool g[233];int dfs(int n) {if (n==0) return 0;if (n==1) return 1;if (g[n]) return f[n];f[n] = dfs(n-2) + dfs(n-1);g[n]=true;return f[n]; }?
動態規劃具有的特點:
狀態:要算算什么;
轉移方程:要怎么算;
無后效性:動態規劃所有的狀態之間組成的有向無環圖;
階段性:我們在求一個狀態時,前面的狀態一定是已經求出來的;
有時候動態規劃不一定是從1 -> n 的,它可能是亂序的;但我們要始終記住它是一個 DAG,所以可以將每個狀態看作一個結點,進行拓撲排序,然后就又變得有序了;
?
動態規劃的常見種類:
1.背包問題:
01背包:
n 個物品,m 容量,每個物品有體積和價值,放入的物品不超過背包容量,求最大化價值和;
第一個維度:f [ i ] 表示我們現在已經放好了前 i 個物品了;
第二個維度:f [ j ] 表示放進去的物品的體積之和是多少;
那么狀態就是:f [ i ][ j ] 代表我們已經嘗試將前 i 個物品都放進去過,體積之和為 j 時所能取到的最大價值;
怎么轉移?
如果第 i+1 個物品不放進去: f [ i ][ j ] = f [ i+1 ][ j ] ;
如果第 i+1 個物品放進去:f [ i+1 ][ j+v[i+1] ] = f [ i ][ j ] + w [ i+1 ];
這種方法是自己更新別人。
我們現在用別人更新自己:
如果第 i 個物品不放進去:f [ i ][ j ] = f [ i-1 ][ j ];
如果第 i 個物品放進去:f [ i ][ j ]= f [ i-1 ][ j-v[i-1] ] + w[ i ];
注意邊算邊取 max;
#include<iostream> #include<cmath>using namespace std;int n,m,w[233],v[233]; int f[233][233];int main() {cin >> n >> m;for (int a=1;a<=n;a++)cin >> v[a] >> w[a];for (int i=1;i<=n;i++)for (int j=0;j<=m;j++){f[i][j] = f[i-1][j];if (j >= v[i]) f[i][j] = max(f[i][j],f[i-1][j-v[i]]+w[i]);}int ans=0;for (int a=0;a<=m;a++)ans = max(ans,f[n][a]);cout << ans << endl;return 0; }
?
?
完全背包:
考慮每個物品可以用無限次的最大價值。
狀態還是不變。
重新考慮下狀態轉移方程:
由于每個物品可以放若干個,所以我們枚舉一下第 i 個物品放了多少個;
#include<iostream>using namespace std;int n,m,w[233],v[233]; int f[233][233];int main() {cin >> n >> m;for (int a=1;a<=n;a++)cin >> v[a] >> w[a];for (int i=1;i<=n;i++)for (int j=0;j<=m;j++)for (int k=0;k*v[i]<=j;k++) //注意上限 f[i][j] = max(f[i][j],f[i-1][j-k*v[i]]+k*w[i]);int ans=0;for (int a=0;a<=m;a++)ans = max(ans,f[n][a]);cout << ans << endl;return 0; }但是時間復雜度升到了O(n3),我們要考慮一下優化:
我們可以在之前01背包的代碼上做個小小的改動就好了:
#include<iostream> #include<cmath>using namespace std;int n,m,w[233],v[233]; int f[233][233];int main() {cin >> n >> m;for (int a=1;a<=n;a++)cin >> v[a] >> w[a];for (int i=1;i<=n;i++)for (int j=0;j<=m;j++){f[i][j] = f[i-1][j];if (j >= v[i]) f[i][j] = max(f[i][j],f[i][j-v[i]]+w[i]); //改動就是這里 }int ans=0;for (int a=0;a<=m;a++)ans = max(ans,f[n][a]);cout << ans << endl;return 0; }為什么這樣是對的呢?原理何在?
我們可以簡單地畫一下這個程序的流程圖:
我們改動之后的那一行的代碼的意思就是在第 i 層上橫著跑,每走一次就是第 i 種物品在原先的基礎上多選一個,無限走下去就實現了選無限個物品;
這樣時間復雜度就被我們降到了O(n2);?
?
有限背包:
考慮每個物品可以用有限次的最大價值。
我們直接枚舉每個物品用多少次:
#include<iostream> #include<cstdio> #include<cmath>using namespace std;int n,m,w[233],v[233],z[233]; int f[233][233];int main() {cin >> n >> m;for (int a=1;a<=n;a++)cin >> v[a] >> w[a] >> z[a];for (int i=1;i<=n;i++)for (int j=0;j<=m;j++)for (int k=0;k<=z[i];k++)f[i][j] = max(f[i][j],f[i-1][j-k*v[i]]+k*w[i]);int ans=0;for (int a=0;a<=m;a++)ans = max(ans,f[n][a]);cout << ans << endl;return 0; }時間復雜度O(n3)級別的,顯然不行,考慮優化一下:?
我們可以將原先的物品捆綁在一起:
假設我們一個物品能用 13 次,那么我們就可以將這 13 個拆成四個捆綁包:
假設我們要選 9 個這種物品,其實就是選上第1,2,4 個捆綁包!
這樣的話,我們原先的有限背包的問題就轉化成了01背包的問題!(判斷選擇那幾個捆綁包)
時間復雜度O(n2k),k 是物品能分成幾個捆綁包;
怎么拆捆綁包?類似于二進制:
如果我們一個物品能用 26 次:
我們可以將 26 拆成:1,2,4,8……,我們接下來要拆 16 了,可是只剩下了 26-1-2-4-8=11,明顯小于 16,所以最后一個包的大小就是11;
為什么要這樣拆分捆綁包呢?換句話說就是為什么這樣能包含所有的情況呢?
最終的疑問還是在最后一個捆綁包11上。
我們看 26 能拆成的所有捆綁包:1,2,4,8,11;
由于前四個數是我們通過二進制分解來的,所以前四個捆綁包能表示1~15的所有情況;
那么對于16~26的情況呢?這時候我們就必須選上最后一個捆綁包11,那么我們還需選5~15,這不就轉化成了前面的情況了嘛?明顯5~15能被前四個捆綁包全部包含。
證畢!
我們發現拆成的捆綁包的個數是 log n,那么時間復雜度就是:O(nm log n);
我們在讀入的時候就要處理一下捆綁包。
#include<iostream> #include<cmath>using namespace std;int n,m,w[233],v[233]; int f[233][233];int main() {cin >> n >> m;int cnt = 0;for (int a=1;a<=n;a++){int v_,w_,z;cin >> v_>> w_ >> z; //z個物品 int x = 1;while (x <= z) //如果能分解出一個完整的捆綁包就分解 {cnt ++; //捆綁包個數加一 v[cnt] = v_*x; //這個捆綁包的體積 w[cnt] = w_*x; //這個捆綁包的價值 z-=x; //還剩下多少個物品 x*=2; //別忘記乘2 }if (z>0) //如果有剩余,單獨作為最后一個捆綁包 {cnt ++;v[cnt] = v_*z;w[cnt] = w_*z;}}n=cnt; //改成捆綁包的數量 for (int i=1;i<=n;i++) //和01背包的代碼一樣 for (int j=0;j<=m;j++){f[i][j] = f[i-1][j];if (j >= v[i]) f[i][j] = max(f[i][j],f[i-1][j-v[i]]+w[i]);}int ans=0;for (int a=0;a<=m;a++)ans = max(ans,f[n][a]);cout << ans << endl;return 0; }?
?
2.基礎動態規劃
經典例題:數字三角形
狀態:f [ i ][ j ] 走到第 i 行第 j 列所經過的最大數字之和最大是多少;
考慮到 f [ i ][ j ] 要么從上面 f [ i-1 ][ j ] 走過來,要么從左上方 f [ i-1 ][ j-1 ] 走過來,所以取個max就好了;
狀態轉移方程: f [ i ][ j ] = max ( f [ i-1 ][ j ] , f [ i-1 ][ j-1 ] ) + a [ i ][ j ];
?
數字三角形2
由于太簡單,加了一個條件:求最后答案 mod 100 最大。
如果我們還是像剛才那樣定義狀態的話是錯的,因為和大的話不一定模數最大,也就是說前面的最優值不能求出后面的最優值;
這個題多一個條件,那么我們就增加一個維度;
定義狀態: f [ i ][ j ][ k ] 我們走到第 i 行第 j 列的位置使得最大值之和模 100 等于 k 是可行不可行的;
狀態轉移方程:
從 ( i , j ) 往下走:f [ i+1 ][ j ][ (k+a[ i+1 ][ j ])%100 ] =1;
從 ( i , j ) 往右下走:f [ i+1 ][ j+1 ][ (k+a[ i+1 ][ j+1 ])%100 ] =1;
初始化:f [ 1 ][ 1 ][ a[1][1]%100 ] = 1;
#include<iostream>using namespace std;bool f[233][233][233];int main() {cin >> n;for (int i=1;i<=n;i++)for (int j=1;j<=i;j++)cin >> a[i][j];f[1][1][a[1][1] % 100] = true;for (int i=1;i<n;i++)for (int j=1;j<=i;j++)for (int k=0;k<100;k++)if (f[i][j][k]){f[i+1][j][(k+a[i+1][j])%100]=true;f[i+1][j+1][(k+a[i+1][j+1])%100]=true;}for (int j=1;j<=n;j++)for (int k=0;k<100;k++)if (f[n][j][k]) ans=max(ans,k);cout << ans << endl;return 0; }?
?
最長上升子序列
狀態設置:f [ i ] 表示 i 這個數最為最后一個數時最長上升子序列的長度;
f [ i ] = max ( f [ j ] + 1 ),1 <= j <= i 且 aj < ai ;
枚舉 j 的時候我們可以用線段樹;
用數據結構來加速動態規劃求值是個常用的方法;
?
3.區間動態規劃
經典例題:合并石子
有 n 堆石子,每次只能合并相鄰兩堆石子,花費的代價是兩堆石子的重量和,求將 n 堆石子合并成 1 堆石子的最小代價;
狀態設置:f [ i ][ j ] 表示將第 i 堆石子合并到第 j 堆石子的最小代價;
初始化:f [ i ][ i ] = 0,把自己合并到自己的代價是0;
狀態轉移方程:
我們一定可以找到一個分界線,使得先使分界線左邊的所有石子合并成一堆,分界線右邊的所有石子合并成一堆,最后將兩堆石子再合并;
所以我們可以枚舉一個中界線 k,左邊的答案就是 f [ i ][ k ],右邊的答案就是 f [ k+1 ][ j ],那么 f [ i ][ j ] = min ( f [ i ][ k ] + f [ k+1 ][ j ] + i~j的區間和 ),區間和的話前綴和就可以維護;
最后的答案就是 f [ 1 ][ n ](把第一堆石子合并到第 n 堆石子);
詳細請看之前我整理的博客(霧 【傳送門】
我們第一個維度應該要枚舉長度:
如果我們是按照左端點從 1~n 枚舉作為第一維度的話,假如我們要求 f [ 1 ][ n ],我們應該是用 f [ 1 ][ i ] + f [ i+1 ][ n ] 來更新答案的,那么,f [ i+1 ][ n ] 算出來了嘛?顯然沒有!因為我們的左端點從小到大,現在才枚舉到1呢,i 肯定還沒有被更新,所以這是錯的!所以我們按照區間長度來枚舉作為第一維是對的:一個長度為 i 的區間一定是由兩個長度小于 i 的區間來更新的,這樣就可以了。
#include<iostream> #include<cstdio> #include<cmath> using namespace std; int read() {char ch=getchar();int a=0,x=1;while(ch<'0'||ch>'9'){if(ch=='-') x=-x;ch=getchar();}while(ch>='0'&&ch<='9'){a=(a<<3)+(a<<1)+(ch-'0');ch=getchar();}return a*x; } int n,a[201],fminx[201][201],fmaxn[201][201]; long long sum[201]; int main() {n=read();for(int i=1;i<=n;i++) {a[i]=read();a[n+i]=a[i];}for(int i=1;i<=n*2;i++) {sum[i]=sum[i-1]+a[i];fminx[i][i]=0;fmaxn[i][i]=0;}for(int i=2;i<=n;i++) //枚舉區間長度 {for(int j=1;i+j<=2*n+1;j++) //枚舉區間左端點 {int r=i+j-1;fmaxn[j][r]=0;fminx[j][r]=1e9;for(int k=j;k<r;k++){fmaxn[j][r]=max(fmaxn[j][k]+fmaxn[k+1][r],fmaxn[j][r]);fminx[j][r]=min(fminx[j][k]+fminx[k+1][r],fminx[j][r]);}fmaxn[j][r]+=sum[r]-sum[j-1];fminx[j][r]+=sum[r]-sum[j-1];} }int minx=1e9,maxn=-1e9;for(int i=1;i<=n;i++) {minx=min(minx,fminx[i][i-1+n]);maxn=max(maxn,fmaxn[i][i-1+n]);}printf("%d\n%d",minx,maxn);return 0; }?
矩陣乘法
計算 n 個矩陣的矩陣乘法,自定義運算順序,問最少需要幾次運算?
兩個矩陣相乘后,就會產生一個新矩陣,所以就是矩陣合并。
狀態定義:f [ l ][ r ] 表示將第 l 個矩陣乘到第 r 個矩陣需要多少次;
狀態轉移方程: f [ l ][ r ] = min ( f [ l ][ k ] + f [ k+1 ][ r ] + al * ak+1 * ar+1 ), l <= k <= r;
代碼參考石子合并那個題。
?
4.狀態壓縮動態規劃
按照選取集合的狀態劃分轉移階段;
轉移方式:枚舉下一個要選取的物品。
?
看個例題:
平面設計有 n 個點,每個點的坐標是(xi , yi?),問從一號點走完所有點最后再回到一號點的最短路徑。
首先每個點沒有必要走兩次,走一次就夠了。
變化量:
1.當前在哪個點;
2.走過哪些點(我們需要從沒走過的點里面選一個走);
狀態設置:f [ s ][ i ] 我現在走到了第 i 個點,走過了哪些點(s);
但是走過哪些點怎么用一個整數表示?
我們就要用到了狀態壓縮:把一個數組壓縮成一個數。
假設我們有五個點:
情況是:我們已經走了1,2,4 這三個結點了,3,5結點還沒有走:
我們將走過的結點的位置寫上1,未走過的結點的位置寫上0:
我們可以將下面這個01串看做是一個二進制的數,然后我們再將其轉化成十進制的數:
這樣的話,我們就將這種情況轉化成了一個數字,這就是狀態壓縮。
邊界條件:
f [ 1 ][ 0 ] = 0,我只走了第 0 個點(1只有第0位有1),當前位置在0,時間是0;
狀態轉移方程:
我們找個沒走過的點走一下就好了。
枚舉一個 j,看看 s 的二進制的第 j 位是不是0,如果是0就走 j ,并把第 j 位改成 1;
注意要先枚舉狀態在枚舉每個點,因為我們走的點是越來越多的;
?
#include<iostream> #include<cmath> #include<cstring>using namespace std; const int inf=1e9; double f[233333][233]; double x[233],y[233],ans; int n;double dis(int a,int b) {return sqrt((x[a]-x[b])*(x[a]-x[b])+(y[a]-y[b])*(y[a]-y[b])); } int main() {cin >> n;for (int a=0;a<n;a++)cin >> x[a] >> y[a];memset(f,0x3f,sizeof(f)); //初始化無窮大 f[1][0]=0; //邊界條件 for (int s=0;s<(1<<n);s++) //枚舉每種狀態 for (int i=0;i<n;i++) //枚舉當前結點是哪個數 if (f[s][i] < inf) {for (int j=0;j<n;j++) //枚舉哪個數沒去過 if ( ((s>>j) & 1) == 0) //如果s的第j位是0,說明沒去過 {int news = s | (1<<j);//新的狀態:將s的第j位變成1 f[news][j] = min(f[news][j],f[s][i] + dis(i,j)); //更新 }}for (int i=0;i<n;i++)ans=min(ans, f[(1<<n)-1][i] + dis(i,0));//枚舉每個終點,然后記得要返回0號結點 cout << ans; return 0; }?
這類問題是旅行商問題(TSP問題),時間復雜度最優為O(2n * n2),能用狀壓DP的話數據要在 n <= 22 的范圍內;
?
在一個種草之后,與之相鄰的四個格子都不能種草了;
狀態定義:f [ i ][ s ] 表示前 i 行的草已經種完了,且第 i 行種的草的長相是 s;
狀態轉移方程:
前 i 行已經種完草了,我們考慮第 i+1 行怎么種草;
第 i+1 行不能連著種草。所以用二進制表示的時候不能有兩個連續的兩個1,由于相鄰的兩列也不能種草,所以第 i 行種草的地方第 i+1 行不能種草。
假設我們第二行的種植情況是這樣的:
由于相鄰的格子不能種植,所以我們可以確定第 i+1 行一定不能種在這些格子上:
其他的位置問你不能確定,但是我們可以發現一個規律:si & si+1 = 0。
所以我們只要先找一個s',使得 s & s' = 0 就行了;
?
?
發現和上一個題沒有什么本質的區別,只是多了個條件;
題目多一個條件直接多加一個維度:
狀態定義:f [ i ][ s ][ j ] 第 i 行的國王已經放完了,已經放了 j 個國王的情況下,第 i 行國王放置的情況為 s;
相比于上個題來說只是再需要判斷一下對角線上也不能放國王就好了。
?
5.數位動態規劃
什么是數位DP?
數位DP 是我們在 DP 的時候是按照數的每一位來進行轉移的動態規劃;
給出兩個數 l , r,問從 l~r 有多少個數。
顯然答案就是:r - l + 1;
但是我們要用數位DP 做!
首先數位DP有個叫前綴和轉化的東西:算[ l , r ] 有多少個數,就是算 [ 0 , r ] 里有多少個數 -? [ 0 , l-1 ] 里有多少個數;
那么問題就轉化成求 [ 0 , x ] 有多少個數。
假設 x = 3245,
實際上就是在問有多少個 y ,使得 0 <= y <= x;
考慮到 x 只有四位,那么 y 最多也只有四位。
也就是說,我們要往四個格子里面填數,問有多少種方案使得填出來的數小于等于 x;
如果從低位往高位填的時候并不知道是否比 x 大還是小,但我們往高位往低位填的時候就能確定了。
狀態設置:f [ i ][ j ] 我們在第 i 位已經填好的情況下,如果 j = 0 代表我們填的數已經小于x,如果 j = 1 代表我們填的數無法確定是等于還是大于,考慮到我們不用算大于的情況,所以 j = 1 代表我們填的數剛好等于 x;
狀態轉移方程:
假設我們已經填好了第 i 位,我們接下來要填的是第 i-1 位(從高往低填),數位DP 的過程就是在求我們這一位是填1~9的哪一位;
初始化:f [ l+1 ][ 1 ] = 1;
我們的 x 只有 l 位,那么我們 l 位的更高位一定是0,所以與 x 一樣的方案數有1種:全部填0;
判定一下將 k 填進去之后會不會比 x 大:
1.如果前幾位都一樣,當時當前填的 k 比 x 的對應位大的話,那么我們不轉移;否則如果小于的話,那么第二維是0,如果正好又等于 x,那么第二維繼續維持 1:
2.如果之前的數就比 x 小了,那么之后不管怎么填始終是小于 x 的,也就是說我們的第二維仍然是0;
最后的答案就是: f [ 1 ][ 0 ] + f [ 1 ][ 1 ];
?
還是數位 DP 前綴和的思想:求 [ 0 , r ] 的數的數位之和 -??[ 0 , l-1 ] 的數的數位之和;
狀態設置:g [ i ][ j ] 我們填好第 i 位后,是等于還是小于的數字之和;
假設我們在一位填了一個 k,每種方案都接了一個 k,填 k 的總貢獻就是:f [ i ][ j ] * k;
所以我們不僅要求所有數的數位之和,還要求方案數,那么我們在上面代碼的基礎上改一改就好了;
#include<iostream>using namespace std;int solve(int x) {int l=0;while (x>0){l++;z[l] = x%10;x/=10;}memset(f,0,sizeof(f));memset(g,0,sizeof(g));f[l+1][1]=1; //邊界條件,l+1位往前都是0,是相等的 g[l+1][1]=0; //前L+1位都是0,和也是0 for (int i=l+1;i>=2;i--) //用自己去算別人 for (int j=0;j<=1;j++) //看看是等于還是小于的情況 for (int k=0;k<=9;k++) //枚舉這一位我們能填什么 {if (j==1 && k>z[i-1]) continue; //如果前面相同了這一位還大于x的對應位,說明不能填 int j_; if (j==0) j_=0; //如果前面填的數已經小于x了,后面再怎么填都小于x了 else if (k==z[i-1]) j_=1; //如果前面的數等于x,并且這一位還是等于x的對應位,那么新的數還是和x相同 else j_=0; //否則的話就小于x f[i-1][j_] += f[i][j];//加法技術原理求方案數 g[i-1][j_] += f[i][j] * k + g[i][j]; //這一位填上個k,對于每一種方案都可以填上k啊,那么總的貢獻就是方案數乘k,別忘了加上之前位數的位數之和 }return g[1][0] + g[1][1]; //答案 }int main() {cin >> l >> r;cout << solve(r) - solve(l-1) << endl;return 0; }?
?
多一個條件多加一個維度。
狀態設置:f [ i ][ j ][ k ] 代表前 i 位已經填好了,j = 0 代表小于,j = 1 代表等于,第 i 位填的是 k;
這樣的話我們就避開差小于 2 的情況;
?
狀態設置:f [ i ][ j ][ r ] 從高向低填到第 i 為,j判斷是否相等,我們已經填的數的數位之積是r;
發現 r 的范圍很大,空間爆內存啊,怎么辦?
因為 r 是各位數相乘的結果,所以 r 的因子里不可能有超過10的質因子;
也就是說,r 里面的質因子只有2,3,5,7,再根據唯一分解定理,那么 r 一定可以表示為2a?* 3b?* 5c?* 7d?;
多加幾個維度:f [ i ][ j ][ a ][ b ][ c ][ d ] 表示從高位往低位填,我們填的數的數位之積是2a * 3b * 5c * 7d;
還可以優化:我們發現 a,b,c,d 不可能同時達到上界,所以我們可以預先處理出 long long 范圍內所有滿足2a?* 3b?* 5c?* 7d?的數,大約有3W多個,然后改一下狀態:f [ i ][ j ][ k ] 表示第 k 個這樣的數,這樣就不會有任何的空間浪費。
?
6.樹形動態規劃
就是在樹上做的DP,注意這棵樹一定是有根樹,否則不能DP;
例題:
給你個 n 個點的樹,問你樹上有多少個點?
n 個啊(大霧
不,我們要用樹形DP!
在每個點,我們維護以它為根的信息;
狀態設置:f [ i ] 表示以 i 為根的子樹有多少個點;
狀態轉移方程:f [ i ] = Σ( j ∈son [ i ] ) f [ j ] + 1;
樹形DP就是把它所有兒子對應的所有信息轉和得到自己的信息;
?
樹的直徑:在樹上找到兩個點,使得這兩個點的距離最遠;
樹的路徑大概長這樣:
我們發現這個路徑就是先向上走到 LCA,再從 LCA 往下走走到另外一個結點;那么我們可以換種角度來看,不就是從 LCA 往下走跳最長路和次長路之和嘛?
所以我們的問題就轉化成:我們求每個點往下走的最長路和次長路。
狀態設置:f [ i ][ 0 ] 代表從第 i 個點向下走最長能走多少,f [ i ][ 1 ] 代表從第 i 個點向下走次長能走多少。
答案:求出每個點的 f [ i ][ 0 ] + f [ i ][ 1 ] ,取最大值。
注意到在算第 i 個點的值得時候,下面的點已經被算過了。
f [ i ][ 0 ] = max (f [ Pj ][ 0 ] )+ 1,Pj?表示是 i 的第 j 個兒子;
選 f [ i ][ 1 ] 的時候,一定要避免與 f [ i ][ 0 ] 選到一個結點上去;
所以我們只需要從每個兒子中找到一條最長的,看看是否能更新就好了;
如果有一個兒子的最長路是大于父親結點的最長路的,那么父親結點現在的次長路更新為原先的最長路,最長路更新為兒子的最長路+1;否則如果兒子的最長路只大于父親的次長路,那就更新父親的次長路;
void dfs(int i) {for (int j=head[i];j;j=e[j].next){int p=e[j].to;dfs(p);}for (int j=head[i];j;j=e[j].next){int p=e[j].to;int v = f[p][0]+1; if (v>f[i][0]) //如果大于父親的最長路 {f[i][1]=f[i][0]; //現在的次長路是原先的最長路 f[i][0]=v; //最長路更新為兒子的最長路+1 }else if (v>f[i][1]) f[i][1]=v;//如果不能更新最長路,那看看能否更新次長路 } }?
狀態設置: f [ i ] 表示以 i 為根的子樹有多少個點;
一條邊會被多少條路徑經過?
我們要統計紅色的這條邊對答案的貢獻,考慮到這條邊的兩側的結點都會經過這一條邊,里面(下面)的結點個數是 f [ i ],外面(右邊)的結點個數是 n - f [ i ],那么下面的一個點到外面的路徑有 n - f [ i ](每個點都要到一遍吧~),那么總共 f [ i ] 個點就有 f [ i ] * (n - f [ i ])跳路徑穿過這條邊,再考慮外面的點每個點還要到達里面的點一次,所以這條邊的貢獻就是:2 * f [ i ] * (n - f [ i ]),那么最后的答案就是 Σ(2 * f [ i ] * (n - f [ i ])),i 枚舉每條邊。
?
?
狀態設置:f [ i ][ 0/1 ] 從以 i 為根的子樹從中選出若干的點的最大值是多少,0 代表 i 這個點沒選,1 代表 i 選了;
最后答案:max (f [ 1 ][ 0 ] , f [ 1 ][ 1 ]);
既然 i 選了,那么 i 的所有兒子都不能選,f [ i ][ 1 ] = Σ f [ j ][ 0 ](j∈son [ i ])+ ai;
如果 i 選了,那么 i 的兒子可以選也可以不選,那么 f [ i ][ 0 ] = Σ max ( f [ j ][ 0 ] , f [ j ][ 1 ] ) (j ∈ son [ i ]);
?
?
狀態設置:f [ i ][ 0/1 ] 表示第 i 個士兵選還是不選;
如果第 i 個士兵不選,那么與兒子相連的邊必須要兒子來看著,那么每個兒子都要選:
f [ i ][ 0 ] = Σ f [ j ][ 1 ](j∈son [ i ]);
如果第 i 個士兵選上了,那么兒子們可選可不選,取最小值;
f [ i ][ 1 ] = Σ min(f [ j ][ 0 ] , f [ j ][ 1 ])(j ∈ son [ i ])+ 1;
?
拓展:?
如果每個士兵只能守護與其距離不超過二的邊呢?
狀態設置:f [ i ][ 0/1/2 ] 以 i 為根的這個子樹已經用士兵覆蓋住了,i 這個結點向下走到達的最近的士兵的距離是 0/1/2;
0:就是這個結點有士兵;
1:兒子結點有士兵;
2:孫子結點有士兵,兒子結點沒有士兵;
f [ i ][ 0 ] = Σ min ( f [ j ][ 0/1/2 ] ) (j ∈ son [ i ])+ 1;
f [ i ][ 1 ] = 由于太danteng,需要再來個DP求!!!
g [ j ][ 0/1 ] 我們已經確定了前 j 個兒子的取值,其中這個這 j 個兒子中有沒有一個兒子拿出一個 0 來作為答案(距離最近的士兵的距離是0)
f [ i ][ 2 ] 也要用類似的DP來求!!!
DP套DP可還行;
轉載于:https://www.cnblogs.com/xcg123/p/11200008.html
總結
以上是生活随笔為你收集整理的7月清北学堂培训 Day 5的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Python图形之-tkinter与ma
- 下一篇: subquery unnesting、S