一直都害怕这个,今天终于下定决心啃这块了。。
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处判断时就不能接受等号,因为需要字典序输出)
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 规划方向