第9章 动态规划初步读书笔记

一直都害怕这个,今天终于下定决心啃这块了。。

9.1 数字三角形:

9.1.1 问题描述和状态定义 —— 非负数字组成的三角形,从第一个数开始每次可以向右或者向下走一格,直到最下行,求沿途经过数和的最大值。这是一个动态决策的问题,每次都面临两种选择,如果用回溯法找所有路线,则n层三角形有2的n次方条的路线,效率很低。所以引入状态转移的概念:

                    把当前位置 ( i, j)看成一个状态,定义函数 d( i, j)为从( i, j)出发后能得到的最大的和,那么可以写出一个递推方程:d( i, j) = a( i, j) + max{ d( i, j + 1), d( i + 1, j + 1)};

       d( i, j + 1)为从( i, j + 1)出发的最大和,即一个最优子结构,也可以描述成“全局最优解包含局部最优解”。上式是一个状态转移方程,状态和状态转移方程是动态规划的核心

9.1.2 记忆化搜索与递推 —— 直接递归很多时候子问题进行了重复计算。

             可以通过一些方法在使用递推的时候避免重复计算

#include
int max( int a, int b){
	return a > b ? a : b;
}
int main(void){
	int d[5][5] = {0}, a[5][5] = {0};
	int i, j;
	// 输入数据
	for( i = 1; i <= 4; i++)
		for( j = 1; j <= i; j++)
			scanf("%d", &a[i][j]);
	// 最底部赋值
	for( j = 1; j <= 4; j++) d[4][j] = a[4][j];
	// 递推计算
	for( i = 3; i > 0; i--){               // 这里的i是倒序枚举的,因为最底部已经被赋值,按照逆序过程进行的遍历,保证前一个状态的值已经被计算出来了
		for( j = 1; j <= i; j++){
			d[i][j] = a[i][j] + max( d[i+1][j] , d[i+1][j+1]);
		}
	}
	printf("%d", d[1][1]);
	return 0;
}
            而记忆化搜索解决了递归时重复计算的问题:

主函数中要先进行初始化:memset( d, -1, sizeof(d)); (一般只用0,-1作为批量初始化的对象)
int dp( int m, int k){
	if( d[m][k] >= 0) return d[m][k];                          // 判断是否已经计算过
	if( m > 4) return 0;
	return d[m][k] = a[m][k] + max( dp( m+1, k), dp(m+1, k+1));
}
9.2 DAG上的动态规划

9.2.1 DAG模型 —— 有向无环图。矩形之间的可嵌套关系是一个二元关系,用图来建模。如果X可以嵌套在Y中,则在X和Y之间连接一条有向边,求最多的嵌套矩阵就是在求DAG上的最长路径问题。

9.2.1 最长路及其字典序 ——“嵌套矩形”中,求DAG不固定起点的最长路径:设d(i) 为从结点 i 出发的最长路径长度。则有 d(i) = max{ d(j) + 1},最终答案是所有d(i) 的最大值

                图的存储涉及到了邻接矩阵 在这个博客里学习的http://www.cnblogs.com/kangyoung/articles/2169183.html

              【在图的邻接矩阵表示法中: ① 用邻接矩阵表示顶点间的相邻关系   ② 用一个顺序表来存储顶点信息 】

               (当然这里的最长路径的值指的是路径所包含的点值,我还迷糊过一阵)

#include
#include
int a[5][5] = {
				0, 1, 0, 1, 0,
				0, 0, 1, 0, 1,
				0, 0, 0, 0, 0,
				0, 0, 1, 0, 0,
				0, 0, 0, 1, 0};
int d[5] = {0};
int dp( int i);
int main(void){
	int b;
	dp(0);
	return 0;
}
int dp( int i){
	int j, flag = 0;
	if( d[i] > 0) return d[i];
	d[i] = 1; 
	for( j = 0; j < 5; j++){
		if( a[i][j]) {
			flag = dp(j) + 1;            // A
			if( d[i] > flag) d[i] = d[i];
			else d[i] = flag;
		}		
	}
	return d[i];                                // 因为忘记了写返回值,结果一直有问题,之后终于知道错误出现在哪了
}

                     返回值忘记写了,就会造成:如在A处函数dp(2)递归开始时数组中 d[2]元素是0,递归完成后d[2]元素虽然是1,但是返回到上一层时没有返回值,没有办法把值赋给flag,造成了flag 一直是1,结果一直报错。花了我好久才查出错来,还是不细心啊

                     书上说有一种引用方法 

int& ans = d[i];  // 任何对ans的读写事实上都是在对d[i]进行,当d[i]换成d[i][j][k][m][n]时会比较简便

                     用字典序消除并列名次:1,2,4,3元素大于 1,2,3,4元素的组合。所以在所有d值算出来以后选择最大的d[i]所对应的i,若并列则选取最小 i ,接下来 d(i) = d(j) + 1 中的任何一个 j ,为了使字典序最小,应选择最小的 j 。代码如下

void print_ans( int i){
	int j;
	printf("%d ", i);
	for( j = 0; j <= 4; j++) if( a[i][j] && d[i] == d[j] + 1){    // 因为所有的最佳值一定是1递增的。即决策时依次确定的,很容易按字典序输出
		print_ans(j);
		break;
	}
}

                      如果要打印所有满足的路径,仅仅删除break是不够的,因为如果两条路径共同的根节点只会打印一次,而叶结点会相继输出,无法完整的打印方案。正确的方法是记录路径上的所有点,递归结束再统一输出(类似DPS?)

                      如果把状态定义成“ d(i) 表示以 i 为终点的最长路径长度”,能求出最优值却无法打印出字典序最小的方案:我觉得因为序列是从起点开始打印的,起点找到的字典序最小的点未必在最优路线中,所以无法生成正确的序列。(毫无疑问,两种方法的 d[] 数组是不同的)

9.2.3 固定终点的最长路和最短路 —— 运用DAG建模的另一个示例问题:有 n 种硬币,面值为 Vi ( i = 1,2...n),每种无限多。给定非负整数 S , 选用多少种硬币使面值之和恰好为S?输出硬币数目的最大值和最小值。 把每种面值看成一个点,表示还要凑足的面值,则初始状态为S,目标状态为0 。若当前状态为 i,每使用一个硬币 j,状态便转移到 i - Vj。固定了终点和起点。所以状态转移方程为 最长路: d[i] = min{ d[i - Vj] + 1 }; ; 最短路: d[i] = max{ d[i - Vj] + 1 };

                最长路(天啦噜这个时间太长了):

int dp( int n){
	int i, flag;
	if( d[n] >= 0) return d[n];
	d[n] = 0;
	for( i = 1; i <= 5; i++){
		flag = 0;
		if( n >= v[i]){                       // B
			flag = dp( n-v[i]) + 1;
			if( d[n] < flag) d[n] = flag;
		}
	}
	return d[n];
}

                 B:判断条件加入“=”的原因是: 这个程序中,因为路径长度可以是0 (S是0),所以需要把 d[] 数组初始化为-1 来表示未被访问过。
                 好吧,这个代码中有一个问题,就是S有可能无法被取到,此时的返回值是 0,而它表示的是已经到达终点。如果把 d[n] 初始化为-1,那么它会表示还没被算过,会丢失劳动成果。所以最好还是使用一个vis[]数组

                同时求最大最小两个值,也可以使用递推来求解

for( i = 1; i < 20; i++) min[i] = 100;
	memset( max, 0, sizeof(max));
	min[0] = max[0] = 0;
	for( i = 1; i <= s; i++)
		for( j = 1; j <= 5; j++)
			if( i >= v[j]){
				min[i] = min[i] >= min[i-v[j]]+1 ? min[i-v[j]]+1 : min[i];  // C
				max[i] = max[i] >= max[i-v[j]]+1 ? max[i-v[j]]+1 : max[i];
                        }
int print( int s){                            // 打印方案
	int i;
	for( i = 1; i <= 5; i++){
		if( s >= v[i] && min[s] == min[s-v[i]]+1){
			printf("\n%d", v[i]);
			print( s-v[i]);
			break;
		}
	}
}
               另外一种打印方案是在递推的时候就记录使用了哪个元素(C处判断时就不能接受等号,因为需要字典序输出)

9.3 0-1 背包问题 —— 多阶段的决策问题
9.3.1 多阶段决策问题 —— 背包问题
              无限背包问题:有n种物品,每种有无穷多个,体积为Vi,重量为Wi,选一些装入容量为C的背包中,重量尽量大
              这个问题在贪心那里曾经出现过,现在使用动态规划的方法来解决: 设 d(i) 是指 i 容量能装入的最大重量 ,我们可以列出状态方程: d(i) = d( i - V[j]) + weight[i]。和前面相比,DAG从无权变成了带权。
              0-1 背包问题:此时每种物品只有一个
              和上一个问题相比,只凭借剩余体积的状态无法进行决策:因为每种物品只能访问一次,而我们无法判断每个物品是否已经用过。此时应该换一种状态方程的模式:引入“多阶段决策问题”,即每一个决策都能完成解的一部分。在回溯法中,每个决策相当于给结点一个新的子树,解的生成对应一颗解答树,结点的层数就是“下一个待填充位置”,也就是动态规划中的“阶段”。那么可以给出新的状态转移方程:d( i, j)表示第 i 个阶段(第i层)剩余容量为 j 的最大重量和。
              d( i, j) = max{ d( i + 1, j ),d( i + 1, j - V[i]) + W[i] },设置边界条件为 i >n 时 d( i, j) = 0; 最终状态为 d( 0, c)。
             也就是把 i 物品装到容量为 j 的背包时的背包最大重量,此时 i 物品可以装入背包也可以不装入背包,求两者的最大值。对应了状态方程中的两种状态。
             一般 0-1 背包的状态方程使用递推会更加理想,因为计算顺序明显。
int main(void){
	int d[10][35] = {0};
	int c = 30, sum = 0, ans = 0;
	int i, j;
	memset( d, -1, sizeof(d));
	for( i = 5; i >= 0; i--){
		for( j = 0; j <= c; j++){
			d[i][j] = i == 5 ? 0: d[i+1][j];
			if( j >= v[i])
			d[i][j] = d[i+1][j] > ( d[i+1][j-v[i]] + w[i]) ? d[i+1][j] : d[i+1][j-v[i]] + w[i] ;
		}
	}
	printf("%d\n", d[0][c]);
             也可以用贪心来检验成果是否可靠
struct A{
	int no;
	int data;
}num[6];
int cmp( const void* a, const void* b){                       // 结构数组的排序
    return ((struct A*)b)->data > ((struct A*)a)->data ? 1 : -1;  
} 
for( i = 0; i < 6; i++){
		num[i].data = w[i] / v[i];
		num[i].no = i;
	}
	qsort( num, 6, sizeof(struct A), cmp);
	for( i = 0; i < 6; i++){
		
		sum += v[num[i].no];
		if( sum > c) break;
		ans += w[num[i].no];
	}
	printf("%d", ans);
9.3.2 规划方向
              还有一种状态定义:用 f( i, j)表示把前 i个装到容积为 j 的背包中所能装的总重量。此时的状态转移方程为:f( i, j) = max{ ( f( i - 1, j),f( i - 1, j - V[i] ) + w[i] ) }; 此时 i 应从小到大循环,最终状态是 f( n, c)。
             这种方法允许加入边计算,而不必把 V, W保留(因为是顺序的)
9.3.3 滚动数组 —— 把 f 变成一维数组,减小空间开销,不过打印比较困难
             此时的递推公式变成了 f [i] = max{ f [ i] , f[ i - V] + W}。因为数组是从上到下,左到右计算的,在计算之前,f [j]里保存的是 f( i-1, j) 而f [j-W]保存的是f( i-1, j-W)。判断后事实上是把状态公式的结果保存在 f [i] 中覆盖掉了原来的值
9.4 递归结构中的动态规划
9.4.1 表达式上的动态规划 —— 最优矩阵连乘
              A1*A2*A3*....*An 整个表达式中一定存在最后一个乘号序号为k,使得表达式变为 B * C,其中B = A1 * A2 *....* Ak, C = Ak * Ak+1 * Ak+2 * ...* An。B和C互不相干所以只需求解B和C的最优值(最优子结构),还可以枚举B中的最后一个乘法。所以最终需要处理的子问题都是 Ai * Ai+1 * Ai+2 * ...* Aj 的最优值(最少多少次乘法)所以状态转移方程就是 f ( i, j) = min{ f ( i , k) + f (k , j) + Pi-1 * Pk * Pj};边界为f( i, i) = 0.此时使用记忆化搜索,递推则使用 j-i 的顺序
9.4.2 凸多边形 —— 最优三角剖分【TBC】
9.4.3 树上的动态规划 —— 树的最大独立集
              如果把 d(i) 表示为以 i 为根结点的子树的最大独立集大小,那么结构 i 只有两种决策,选或者不选,如果选 i,则表示 i 的儿子都不能选,转化为了求i 的孙子的所有 d 值之和,不过不选 i,则求i 的儿子的所有 d 值之和。状态转移方程为 d(i) = max{ 1 + Ed(j)( j 属于孙子集), Ed(j)( j 属于儿子集) }。程序编写上可以枚举儿子集和孙子集,求出d(i)后更新 i 的父亲和祖父节点的累加值Ed(j),也称刷表法
9.5 集合上的动态规划 —— 【TBC】

你可能感兴趣的:(刘汝佳的一些题)