算法期末备考-第4练
【主要内容】
回顾旧知识 回溯法(子集和,数独)
学习新知识 动态规划(数字三角形,矩阵连乘,石子合并)
子集和
【题目描述】
子集和问题的一个实例为<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 */