【主要內容】
回顧舊知識 回溯法(子集和,數獨)
學習新知識 動態規划(數字三角形,矩陣連乘,石子合並)
子集和
【題目描述】
子集和問題的一個實例為<S,c>。其中S={x1,x2,…,xn}是一個正整數的集合,c是一個正整數。子集和問題判定是否存在S的一個子集S1,使得S1中所有元素的和為c。 試設計一個解子集和問題的回溯法。
5 10 2 2 6 5 4
【樣例輸出】
2 2 6
【題解】
如同回溯法解決01背包問題一樣,這里子集和,問題等價為背包容量為10,如下有2,2,6,5,4五個物品,價值和重量都一樣 。不過還是利用0代表不取,1代表取其物品。
構建搜索二叉樹,每一層都是一個物品,左子樹是取,右子樹是不取,當走到某一葉子結點時該結點的價值之和為10時,說明有一組解
1 #include<cstdio> 2 3 const int N = 30 ; 4 int vis[N] ; 5 int a[N] ; 6 int n , k ; 7 bool flag = false ; 8 void dfs( int step , int sum ){ 9 if( sum == k ){ 10 for( int i = 1 ; i <= n ; i++ ) 11 if( vis[i] ) printf("%d ",a[i]); 12 putchar('\n'); 13 flag = true ; 14 return ; 15 } 16 if( step == n + 1 ) return ; 17 if( sum + a[step] <= k ){ 18 vis[step] = 1 ; 19 dfs( step + 1 , sum + a[step] ); 20 vis[step] = 0 ; 21 } 22 dfs( step + 1 , sum ); 23 } 24 int main() 25 { 26 scanf("%d%d",&n,&k); 27 for( int i = 1 ; i <= n ; i++ ){ 28 scanf("%d",&a[i]); 29 } 30 dfs( 1 , 0 ); 31 if( !flag ){ 32 printf("-1\n"); 33 } 34 return 0 ; 35 }
數獨問題
【題目描述】
0,0,5,3,0,0,0,0,0, 8,0,0,0,0,0,0,2,0, 0,7,0,0,1,0,5,0,0, 4,0,0,0,0,5,3,0,0, 0,1,0,0,7,0,0,0,6, 0,0,3,2,0,0,0,8,0, 0,6,0,5,0,0,0,0,9, 0,0,4,0,0,0,0,3,0, 0,0,0,0,0,9,7,0,0
給定如上9*9的方格,已經填了一部分的數字,請求出該數獨。
【題解】
做法如同“幻方數”,搜索時按照順序即可,從左往右,從上到下,如果遇到已填入的數字直接跳過,否則,填入一個合法(行,列,小九宮格為出現過的)的數字。一直搜索,只要合法就往下走,若填入數字不合法會因為后面的方格中無法填入其他數字而返回,即回溯回來,再填入其他的數字,直到整個方格都被填完。
【小技巧】
給定(x,y)如何得知在哪一個小九宮格中的?
x/3*3 + y / 3 ,代碼中x,y都是從0開始的,所以我們的九宮格的坐標0~8.
小九宮格的位置也是0~8
1 //Sudoku 2 #include<cstdio> 3 const int N = 9; 4 int a[N][N] = { 5 0,0,5,3,0,0,0,0,0, 6 8,0,0,0,0,0,0,2,0, 7 0,7,0,0,1,0,5,0,0, 8 4,0,0,0,0,5,3,0,0, 9 0,1,0,0,7,0,0,0,6, 10 0,0,3,2,0,0,0,8,0, 11 0,6,0,5,0,0,0,0,9, 12 0,0,4,0,0,0,0,3,0, 13 0,0,0,0,0,9,7,0,0 14 }; 15 int row[N][N] , col[N][N] , ceil[N][N] ; 16 int n = 9 ; 17 void dfs( int x , int y ){ 18 if( x == n ){ 19 for( int i = 0 ; i < n ; i++ ){ 20 for( int j = 0 ; j < n ; j++ ){ 21 printf("%d%c",a[i][j],j==n-1?'\n':' '); 22 } 23 } 24 return ; 25 } 26 if( a[x][y] ){ 27 if( y == n - 1 ){ 28 dfs( x + 1 , 0 ); 29 }else{ 30 dfs( x , y + 1 ); 31 } 32 }else{ 33 int No = x / 3 * 3 + y / 3 ; 34 for( int i = 1 ; i <= n ; i++ ){ 35 if( row[x][i] == 0 && col[y][i] == 0 && ceil[No][i] == 0 ){ 36 row[x][i] = col[y][i] = ceil[No][i] = 1 ; 37 a[x][y] = i ; 38 if( y == n - 1 ){ 39 dfs( x + 1 , 0 ); 40 }else{ 41 dfs( x , y + 1 ); 42 } 43 row[x][i] = col[y][i] = ceil[No][i] = 0 ; 44 a[x][y] = 0 ; 45 } 46 } 47 } 48 } 49 50 int main() 51 { 52 for( int i = 0 ; i < n ; i++ ){ 53 for( int j = 0 ; j < n ; j++ ){ 54 int x = a[i][j] ; 55 if( a[i][j] ){ 56 row[i][x] = 1 ; 57 col[j][x] = 1 ; 58 ceil[i/3*3+j/3][x] = 1 ; 59 } 60 printf("%d%c",a[i][j],j==n-1?'\n':' '); 61 } 62 } 63 puts("The Answer:"); 64 dfs( 0 , 0 ); 65 return 0 ; 66 }
數字三角形
【題目鏈接】https://www.acwing.com/problem/content/900/
【題目描述】
給定一個如下圖所示的數字三角形,從頂部出發,在每一結點可以選擇移動至其左下方的結點或移動至其右下方的結點,一直走到底層,要求找出一條路徑,使路徑上的數字的和最大。
7 3 8 8 1 0 2 7 4 4 4 5 2 6 5 The Answer : 30
【題解】
最基礎的動態規划問題,其中展示了動態規划的“最優子結構”以及“自底向上的方式計算最優值”。
這個問題如果從上到下來看,每一個結點都有兩個選擇走到下一層,那么到達最底層的方式會隨着層數的增加而呈現指數級的增長。即有n層,如果把所有的路徑走一遍取最大值則復雜度為:O(2^n)。
若問題從下往上看,每一個結點對應着一個父節點,父節點對於與其相連的子結點選擇其最大值與自身相加,問題遞歸上去,則我們可以找到這個數字三角形一個路徑中獲取最大值。
得出狀態轉移方程:
1 //動態規划 -> 數字三角形 2 #include<cstdio> 3 #include<algorithm> 4 using namespace std; 5 const int N = 505 ; 6 int a[N][N] ; 7 int dp[N][N] ; 8 9 int main() 10 { 11 int n ; 12 scanf("%d",&n); 13 for( int i = 1 ; i <= n ; i++ ){ 14 for( int j = 1 ; j <= i ; j++ ){ 15 scanf("%d",&a[i][j]); 16 } 17 } 18 19 for( int i = n ; i >= 1 ; i-- ){ 20 for( int j = 1 ; j <= n ; j++ ){ 21 dp[i][j] = a[i][j] + max( dp[i+1][j] , dp[i+1][j+1] ) ; 22 } 23 } 24 printf("%d\n",dp[1][1]); 25 return 0 ; 26 }
矩陣連乘問題
【題目描述】
給定n個矩陣,其中兩個相鄰的矩陣是可乘的,試求出最佳計算次序,使得總計算量最少。
【題解】
由於做矩陣乘法時,復雜度取決於矩陣的維度。雖然矩陣乘法后的結果是一樣。但是過程中由於計算順序不同(滿足結合律)而導致矩陣乘法所付出的代價的不同。
上課時已經推導過,我們只關注最后一次的矩陣乘法的代價,這好比數字三角形從下往上時找最大子結點即可。同樣的道理(最優子結構)運用到這道題目上去推導出狀態轉移方程為:

【具體代碼】
1 //動態規划 - 矩陣連乘 2 #include<cstdio> 3 #include<algorithm> 4 using namespace std; 5 const int N = 1e3 + 10 ; 6 7 //分別是矩陣的維數,區間內矩陣連乘的最小代價,最小代價最后分割的乘法的位置 8 int A[N]; 9 int f[N][N] ; 10 int path[N][N] ; 11 12 //遞歸打印路徑,二叉樹的先根遍歷 13 void dfs_path( int L , int R ){ 14 if( L == R ) { 15 printf(" A%d ",L); 16 }else { 17 printf("("); 18 dfs_path(L, path[L][R]); 19 printf("*"); 20 dfs_path(path[L][R] + 1, R); 21 printf(")"); 22 } 23 } 24 int main() 25 { 26 int n ; 27 scanf("%d",&n); 28 for( int i = 0 ; i <= n ; i++ ){ 29 scanf("%d",&A[i]); 30 } 31 //區間dp套路 32 //第一層枚舉長度 33 for( int Len = 2 ; Len <= n ; Len ++ ){ 34 //第二層枚舉左端點 35 for( int L = 1 ; L + Len - 1 <= n ; L ++ ){ 36 //同時計算出右端點 37 int R = L + Len - 1 ; 38 //初始化需計算的區間 39 f[L][R] = f[L+1][R] + A[L-1] * A[L] * A[R] ; 40 path[L][R] = L ; 41 //枚舉中間切點 k [L,R) 42 for( int k = L ; k < R ; k ++ ){ 43 //計算其代價 44 int t = min( f[L][R] , f[L][k] + f[k+1][R] + A[L-1]*A[k]*A[R] ); 45 //若其代價小於當前值則更新 46 if( t < f[L][R] ){ 47 path[L][R] = k ; 48 f[L][R] = t ; 49 } 50 } 51 } 52 } 53 54 //輸出答案及打印路徑 55 printf("%d\n",f[1][n]); 56 dfs_path( 1 , n ); 57 return 0; 58 } 59 60 /* 61 6 62 30 35 15 5 10 20 25 63 64 15125 65 */
石子合並
【題目鏈接】
https://www.acwing.com/problem/content/284/
【題目描述】
設有N堆石子排成一排,其編號為1,2,3,…,N。
每堆石子有一定的質量,可以用一個整數來描述,現在要將這N堆石子合並成為一堆。
每次只能合並相鄰的兩堆,合並的代價為這兩堆石子的質量之和,合並后與這兩堆石子相鄰的石子將和新堆相鄰,合並時由於選擇的順序不同,合並的總代價也不相同。
例如有4堆石子分別為 1 3 5 2, 我們可以先合並1、2堆,代價為4,得到4 5 2, 又合並 1,2堆,代價為9,得到9 2 ,再合並得到11,總代價為4+9+11=24;
如果第二步是先合並2,3堆,則代價為7,得到4 7,最后一次合並代價為11,總代價為4+7+11=22。
問題是:找出一種合理的方法,使總的代價最小,輸出最小代價。
【輸入】
4
1 3 5 2
【輸出】
22
【題解】
如果理解了矩陣連乘的道理的話,其實這個題目就是一樣的,僅僅是計算的代價不一樣。
寫出狀態轉移方程直接計算即可:

【具體代碼】
1 #include<cstdio> 2 #include<cstring> 3 #include<algorithm> 4 using namespace std; 5 const int N = 1e3 + 10 ; 6 int sum[N] ; 7 int f[N][N] ; 8 int stone[N]; 9 int main() 10 { 11 int n ; 12 scanf("%d",&n); 13 //輸入 + 預處理前綴和 14 for( int i = 1 ; i <= n ; i++ ){ 15 scanf("%d",&stone[i]); 16 sum[i] = sum[i-1] + stone[i] ; 17 } 18 //初始化,因為答案是最小代價,所以把所有位置初始化最大值 19 memset( f , 0x3f , sizeof f ); 20 //石子自己和自己,因不能進行合並產生代價,所以本身代價為0 21 for( int i = 1 ; i <= n ; i++ ) f[i][i] = 0 ; 22 23 //f[L,R] = min{ f[L,k] + f[k+1,R] + sum( L , R ) } 24 //區間dp在計算時,中間部分的必須提前計算. 25 26 //因而枚舉長度從小到大. 27 for( int Len = 2 ; Len <= n ; Len ++ ){ 28 //枚舉左端點 29 for( int L = 1 ; L + Len - 1 <= n ; L++ ){ 30 //算出右端點 31 int R = L + Len - 1 ; 32 //枚舉中間斷點 33 for( int k = L ; k < R ; k ++ ){ 34 f[L][R] = min( f[L][R] , f[L][k] + f[k+1][R] + sum[R] - sum[L-1] ); 35 } 36 } 37 } 38 printf("%d\n",f[1][n]); 39 return 0 ; 40 } 41 42 /* 43 44 4 45 1 3 5 2 46 47 22 48 */
【加強版-石子合並環形版】
好比小學時計算圓柱的側面積一樣,圓柱側面展開其實是長方形。
環形 => 把我們n個為一排,變成 2*n為一排,最后答案必定是1~n分別作為左端點長度為n的區間。
即Answer = min { f[1,n] , f[2,n+1] , f[3,n+2]……f[n,2*n] }
1 #include<cstdio> 2 #include<cstring> 3 #include<algorithm> 4 using namespace std; 5 const int N = 1e3 + 10 ; 6 int sum[N] ; 7 int f[N][N] ; 8 int stone[N]; 9 10 int main() 11 { 12 int n ; 13 scanf("%d",&n) ; 14 for( int i = 1 ; i <= n ; i++ ){ 15 scanf("%d",&stone[i]) ; 16 stone[i+n] = stone[i] ; 17 sum[i] = sum[i-1] + stone[i] ; 18 } 19 for( int i = n ; i <= 2 * n ; i++ ){ 20 sum[i] = sum[i-1] + stone[i] ; 21 } 22 23 memset( f , 0x3f , sizeof f ); 24 for( int i = 1 ; i <= 2 * n ; i++ ) f[i][i] = 0 ; 25 26 for( int Len = 2 ; Len <= n ; Len ++ ){ 27 for( int L = 1 ; L + Len - 1 <= 2*n ; L++ ){ 28 int R = L + Len - 1 ; 29 for( int k = L ; k < R ; k ++ ){ 30 f[L][R] = min( f[L][R] , f[L][k] + f[k+1][R] + sum[R] - sum[L-1] ); 31 } 32 } 33 } 34 int ans = 0x3f3f3f3f ; 35 for( int i = 1 ; i <= n ; i++ ){ 36 ans = min( ans , f[i][i+n-1] ); 37 } 38 printf("%d\n",ans); 39 return 0 ; 40 } 41 42 /* 43 44 4 45 4 4 5 9 46 47 43 48 */
