1 / 42

程序设计实习

程序设计实习. 第八讲 动态规划. 动态规划与搜索. 当我们面临的问题是 , 寻找一个操作序列 , 将初态演化成目标状态 . 计算解空间的元素时 , 决定计算效率的关键因素是所产生的状态数量 判定重复的状态 判定是否能导出目标状态 搜索 : 从初态出发 , 演化出目标状态 演化规则 : 状态之间的变换关系 , S S 1 ||…|| S S k 对状态的处理 : 有序的使用各备选操作 , 分别实施状态变换产生一个新的状态 动态规划 : 从终态出发 , 反演出目标状态 演化规则 : 状态之间的依赖关系 , S S 1 …S k

Download Presentation

程序设计实习

An Image/Link below is provided (as is) to download presentation Download Policy: Content on the Website is provided to you AS IS for your information and personal use and may not be sold / licensed / shared on other websites without getting consent from its author. Content is provided to you AS IS for your information and personal use only. Download presentation by click this link. While downloading, if for some reason you are not able to download a presentation, the publisher may have deleted the file from their server. During download, if you can't get a presentation, the file might be deleted by the publisher.

E N D

Presentation Transcript


  1. 程序设计实习 第八讲 动态规划

  2. 动态规划与搜索 • 当我们面临的问题是, 寻找一个操作序列,将初态演化成目标状态. 计算解空间的元素时, 决定计算效率的关键因素是所产生的状态数量 • 判定重复的状态 • 判定是否能导出目标状态 • 搜索: 从初态出发, 演化出目标状态 • 演化规则: 状态之间的变换关系, SS1||…||SSk • 对状态的处理: 有序的使用各备选操作, 分别实施状态变换产生一个新的状态 • 动态规划: 从终态出发, 反演出目标状态 • 演化规则: 状态之间的依赖关系, SS1…Sk • 对状态的处理: • 使用演化操作, 产生一组新的状态 • “迭加”各新状态的目标状态, 实现状态变换 • 终态与目标状态 • 终态: 不能从该状态演化出其它的状态 • 目标状态: 符合条件的状态

  3. 例1 POJ 2753 Fibonacci数列 • 求 Fibonacci数列的第n项 要有一个长度至少为N的Fibonacci数列 状态: (第K个Fibonacci数的值, 已有Fibonacci数列的长度L) (K, L)对应的目标状态: (K, K+x), x>=0 终态: (2, 2) 初态: (N, 0) 目标状态: (N, N) 演化规则: (K, K)(K-1, K-1)(K-2, K-2)

  4. 动态规划: 递归确定需要反演的最小状态集合 A[0]=0; A[1]=1; for(i=2; i<n; i++) A[i]=-1; int f(int x){ if ( A[x-2]<0 ) A[x-2] = f(x-2); if ( A[x-1]<0 ) A[x-1] = f(x-1); A[x] = A[x-1]+A[x-2]; return A[x]; }

  5. 动态规划: 按照状态演化规则有序反演新的状态 终态: (2, 2) 目标状态: N int f(int x){ int y; A[0]=0; A[1]=1; while(y<x) { A[y] = A[y-1]+A[y-2]; y++; } return A[x-1]; }

  6. 例2POJ1163数字三角形 7 3 8 8 1 0 2 7 4 4 4 5 2 6 5 在上面的数字三角形中寻找一条从顶部到底边的路径,使得路径上所经过的数字之和最大。路径上的每一步都只能往左下或右下走。只需要求出这个最大和即可,不必给出具体路径。 三角形的行数大于1小于等于100,数字为 0 - 99

  7. 输入格式: • //三角形行数。下面是三角形 7 3 8 8 1 0 2 7 4 4 4 5 2 6 5 要求输出最大和 • 状态: D(i, j, sumi,j), 表示从顶点(i, j)到底端的目标路径上各数字之和 • 目标状态: D(i, j, sumi,j), sumi,j>=ai,j, ai,j是顶点(i, j)的值 • 终态: D(4, 0, a4, 0), D(4, 1 , a4, 1), D(4, 2 , a4, 2), D(4, 3 , a4, 3), D(4, 4 , a4, 4) • 初始状态: D(0, 0, -1) • 演化规则: D(i, j, sumi,j)D(i+1, j, sumi+1,j)D(i+1, j+1, sumi+1,j+1)

  8. 动态规划1: 递归确定需要反演的最小状态集合 #include <iostream.h> #define MAX 101 int triangle[MAX][MAX]; int D[MAX][MAX]; int n; int longestPath(int i, int j) { if(D[i+1, j]<0) D[i+1, j]= longestPath(i+1,j); if(D[i+1, j+1]<0) D[i+1, j]= longestPath(i+1,j+1); if(D[i+1, j]<D[i+1, j+1]) D[i, j]=D[i+1, j+1]+triangle[i][j]; else D[i, j]=D[i+1, j]+triangle[i][j]; return D[i][j]; }

  9. void main(){ int i,j; cin >> n; for(i=0;i<n;i++) for(j=0;j<=i;j++) cin >> triangle[i][j]; for(i=0;i<n-1;i++) for(j=0;j<=i;j++) D[i,j]=-1; for(i=0;i<n;i++) D[n-1,i]=triangle[n-1][i]; cout << longestPath(0,0) << endl; }

  10. 动态规划2: 递归确定需要反演的状态集合(不判重) #include <iostream.h> #define MAX 101 int triangle[MAX][MAX]; int n; int longestPath(int i, int j) { if(i==n-1) return triangle[i][j]; int x = longestPath(i+1,j); int y = longestPath(i+1,j+1); if(x<y) x=y; return x+triangle[i][j]; } void main(){ int i,j; cin >> n; for(i=0;i<n;i++) for(j=0;j<=i;j++) cin >> triangle[i][j]; cout << longestPath(0,0) << endl; }

  11. 动态规划3 :按照演化规则有序反演新的状态 #include <iostream.h> #define MAX 101 int triangle[MAX][MAX]; int D[MAX][MAX]; int n; int longestPath(int i, int j) { for( x = n-1 ; x> i ; x -- ) for( y = 0; y < x ; y ++ ) if(D[x, y]<D[x, y+1]) D[x-1, y]=D[x, y+1]+triangle[x-1][y]; else D[x-1, y]=D[x, y]+triangle[x-1][y]; return D[i][j]; }

  12. void main(){ int i,j; cin >> n; for(i=0;i<n;i++) for(j=0;j<=i;j++) cin >> triangle[i][j]; for(i=0;i<n-1;i++) for(j=0;j<=i;j++) D[i,j]=-1; for(i=0;i<n;i++) D[n-1,i]=triangle[n-1][i]; cout << longestPath(0,0) << endl; }

  13. 动态规划3 :按照演化规则有序反演新的状态 #include <iostream.h> #define MAX 101 int triangle[MAX][MAX]; int D[MAX]; int n; int longestPath(int i, int j) { for( x = n-1 ; x> i ; x -- ) for( y = 0; y < x ; y ++ ) if(D[y]<D[y+1]) D[y]=D[y+1]+triangle[x-1][y]; else D[y]=D[y]+triangle[x-1][y]; return D[i][j]; }

  14. void main(){ int i,j; cin >> n; for(i=0;i<n;i++) for(j=0;j<=i;j++) cin >> triangle[i][j]; for(i=0;i<n;i++) D[i]=triangle[n-1][i]; cout << longestPath(0,0) << endl; }

  15. 数字三角型:四种解法的开销比较 7 3 8 8 1 0 2 7 4 4 4 5 2 6 5 • 递归反演1: • 递归调用 • 判定重复状态时使用二维数组 • 递归反演2: • 递归调用 • 无重复状态判断:时间复杂度为 2n,对于 n = 100,肯定超时 • 按规则反演1: • 判定重复状态时使用二维数组 • 按规则反演2 : • 判定重复状态时使用一维数组 • 在这个问题中, 按规则反演隐含了重复状态判断

  16. 例题:最长上升子序列 问题描述 一个数的序列bi,当b1 < b2 < ... < bS的时候,我们称这个序列是上升的。对于给定的一个序列(a1, a2, ..., aN),我们可以得到一些上升的子序列(ai1, ai2, ..., aiK),这里1 <= i1 < i2 < ... < iK <= N。比如,对于序列(1, 7, 3, 5, 9, 4, 8),有它的一些上升子序列,如(1, 7), (3, 4, 8)等等。这些子序列中最长的长度是4,比如子序列(1, 3, 5, 8). 你的任务,就是对于给定的序列,求出最长上升子序列的长度。

  17. 输入数据 输入的第一行是序列的长度N (1 <= N <= 1000)。第二行给出序列中的N个整数,这些整数的取值范围都在0到10000。 输出要求 最长上升子序列的长度。 输入样例 7 1 7 3 5 9 4 8 输出样例 4

  18. 问题分析 • 记目标序列为b1…bk,有两种情况: • 目标序列包含aN: b1…bk-1为从a1到bk-1的最长上升子序列, 且bk-1<aN • 目标序列不包含aN: b1…bk为从a1到bk的最长上升子序列, 且bk>=aN • 状态(k, lenk , maxk): 从a1到ak的最长上升子序列的长度为lenk , 该最长上升序列的最大值为maxk • lenk = max{lenk : i<k && ai<max(maxi, ak)}+1 • 目标状态: (N, lenN , maxN): lenN>1 • 终态:(1, 1, a1) • 演化规则: (k, lenk , maxk)(1, 1, a1)… (k-1, lenk-1 , maxk-1)

  19. 动态规划: 递归确定需要反演的最小状态集合 #include <stdio.h> #include <memory.h> #define MAX_N 1000 int b[MAX_N]; struct { int len; int value; } aMax[Max_N] int maxIncreasingSequence(int i); main() { int N; scanf("%d", & N); for( int i = 0;i < N;i ++ ) scanf("%d", & b[i]); aMax[0].value = b[0]; aMax[0].len = 1; for( i = 1; i < N; i ++ ) aMax[i].len = 0; printf("%d\n", maxIncreasingSequence(N)); }

  20. int maxIncreasingSequence(int i) { int temp, maxValue, j; if(aMax[i-1].len==0) maxIncreasingSequence (i-1); temp = 0; for(j=0; j<i; j++) { if (b[i]<aMax[j].value) maxValue=aMax[j].value; else maxValue=b[i]; if ( b[j]>=maxValue||aMax[temp].len>=aMax[j].len ) continue; temp = j; } aMax[i].len = aMax[temp].len + 1; if ( aMax[temp].value>b[i] ) aMax[i].value = b[i]; else aMax[i].value = aMax[temp].value; return aMax[i].len; }

  21. 动态规划 :按照演化规则有序反演新的状态 #include <stdio.h> #include <memory.h> #define MAX_N 1000 int b[MAX_N]; struct { int len; int value; } aMax[Max_N] int maxIncreasingSequence(int i); main() { int N; scanf("%d", & N); for( int i = 0;i < N;i ++ ) scanf("%d", & b[i]); aMax[0].value = b[0]; aMax[0].len = 1; for( i = 1; i < N; i ++ ) aMax[i].len = 0; printf("%d\n", maxIncreasingSequence(N)); }

  22. int maxIncreasingSequence(int i) { int temp, maxValue, j, x; for ( x=1; x<i; x++ ) { temp = 0; for(j=0; j<x; j++) { if (b[x]<aMax[j].value) maxValue=aMax[j].value; else maxValue=b[x]; if ( b[j]>=maxValue||aMax[temp].len>=aMax[j].len ) continue; temp = j; } aMax[x].len = aMax[temp].len + 1; if ( aMax[temp].value>b[x] ) aMax[x].value = b[x]; else aMax[x].value = aMax[temp].value; } return aMax[i].len; }

  23. 例题: Poj 1458 最长公共子序列 给出两个字符串,求出这样的一个最长的公共子序列的长度:子序列中的每个字符都能在两个原串中找到,而且每个字符的先后顺序和原串中的先后顺序一致。

  24. Sample Input abcfbc abfcab programming contest abcd mnp Sample Output 4 2 0 最长公共子序列

  25. 问题分析 • (i, j, maxLeni,j)表示: s1的左边i+1个字符形成的子串,与s2左边的j+1个字符形成的子串的最长公共子序列的长度为maxLeni,j,有两种情况: • S1[i]==s2[j]: maxLeni,j= maxLeni-1,j-1 +1 • S1[i]!=s2[j]: maxLeni,j=max(maxLeni,j-1, maxLeni-1,j) • (i, j, maxLeni,j)的目标状态: maxLeni,j>=0 • 初始状态: (M,N, -1) • M: strlen(s1) • N: strlen(s2) • 终态: { (i,0,0): 0<=i<M}{ (0,i, 0)==0: 0<=i<N} • 演化规则 (i,j ,maxLeni,j) (i-1,j-1 ,maxLeni-1,j-1)(i,j-1 ,maxLeni,j-1)(i-1,j ,maxLeni-1,j)

  26. 动态规划: 递归确定需要反演的最小状态集合 char str1[1000], str2[1000]; int maxLen[1000][1000]; int maxSubString(int i, int j) { if ( i==0 || j==0 ) return 0; if ( str1[i]==str2[j] ) { if ( maxLen[i-1][j-1]<0 ) maxLen[i-1][j-1]=maxSubString(i-1, j-1); maxLen[i][j] = maxLen[i-1][j-1]+1; return maxLen[i][j]; } if ( maxLen[i][j-1]<0 ) maxLen[i][j-1]=maxSubString(i, j-1); if ( maxLen[i-1][j]<0 ) maxLen[i-1][j]=maxSubString(i-1, j); if ( maxLen[i][j-1]<maxLen[i-1][j] ) maxLen[i][j] = maxLen[i-1][j]; else maxLen[i][j] = maxLen[i][j-1]; return maxLen[i][j]; }

  27. 动态规划:按照演化规则有序反演新的状态 char str1[1000], str2[1000]; int maxLen[1000][1000]; int maxSubString(int i, int j) { int x, y; for ( x=0; x<i; x++ ) { for ( y=0; y<j; y++ { if ( str1[x+1]==str2[y+1] ) { maxLen[x+1][y+1]=maxLen[x][y]+1; continue; } if ( maxLen[x][y+1]<maxLen[x+1][y] ) maxLen[x+1][y+1]=maxLen[x+1][y]; else maxLen[x+1][y+1]=maxLen[x][y+1]; } } return maxLen[i][j]; }

  28. 两种动态规划实现方法的比较 • 递归确定需要反演的最小状态集合 • 所产生的每个状态需要一次递归调用 • 所产生的每个状态都是实施初态的状态变换时必须的 • 按照演化规则有序反演新的状态 • 所产生的部分状态对目标状态没有贡献 • 是否用递归函数实现动态规划? 关键在于是否有利于减少所反演的新状态的数量

  29. 参考实现代码 #include <iostream.h> #include <string.h> char sz1[1000]; char sz2[1000]; int anMaxLen[1000][1000]; main() { while( cin >> sz1 >> sz2 ) { int nLength1 = strlen( sz1); int nLength2 = strlen( sz2); int nTmp; int i,j; for( i = 0;i <= nLength1; i ++ ) anMaxLen[i][0] = 0; for( j = 0;j <= nLength2; j ++ ) anMaxLen[0][j] = 0;

  30. for( i = 1;i <= nLength1;i ++ ) { for( j = 1; j <= nLength2; j ++ ) { if( sz1[i-1] == sz2[j-1] ) anMaxLen[i][j] = anMaxLen[i-1][j-1] + 1; else { int nLen1 = anMaxLen[i][j-1]; int nLen2 = anMaxLen[i-1][j]; if( nLen1 > nLen2 ) anMaxLen[i][j] = nLen1; else anMaxLen[i][j] = nLen2; } } } cout << anMaxLen[nLength1][nLength2] << endl; } }

  31. 例题: POJ 1661 Help Jimmy "Help Jimmy" 是在下图所示的场景上完成的游戏:

  32. 场景中包括多个长度和高度各不相同的平台。地面是最低的平台,高度为零,长度无限。 Jimmy老鼠在时刻0从高于所有平台的某处开始下落,它的下落速度始终为1米/秒。当Jimmy落到某个平台上时,游戏者选择让它向左还是向右跑,它跑动的速度也是1米/秒。当Jimmy跑到平台的边缘时,开始继续下落。Jimmy每次下落的高度不能超过MAX米,不然就会摔死,游戏也会结束。 设计一个程序,计算Jimmy到地面时可能的最早时间。

  33. 输入数据 第一行是测试数据的组数t(0 <= t <= 20)。每组测试数据的第一行是四个整数N,X,Y,MAX,用空格分隔。N是平台的数目(不包括地面),X和Y是Jimmy开始下落的位置的横竖坐标,MAX是一次下落的最大高度。接下来的N行每行描述一个平台,包括三个整数,X1[i],X2[i]和H[i]。H[i]表示平台的高度,X1[i]和X2[i]表示平台左右端点的横坐标。1 <= N <= 1000,-20000 <= X, X1[i], X2[i] <= 20000,0 < H[i] < Y <= 20000(i = 1..N)。所有坐标的单位都是米。 Jimmy的大小和平台的厚度均忽略不计。如果Jimmy恰好落在某个平台的边缘,被视为落在平台上。所有的平台均不重叠或相连。测试数据保Jimmy一定能安全到达地面。

  34. 输出要求 对输入的每组测试数据,输出一个整数,Jimmy到地面时可能的最早时间。 输入样例 1 3 8 17 20 0 10 8 0 10 13 4 14 3 输出样例 23

  35. 问题分析 • 状态(i, j, ti,j): Jimmy处于第i个平台上位置j时, 达到地面所需要的最少时间是ti,j • X1[i]j X2[i] • left(i): Jimmy沿第i个平台向左跑能够到达的下一块木板 • right(i): Jimmy沿第i个平台向右跑能够到达的下一块木板 • 分三种情况 • h[i]-h[left(i)]>MAX: ti,j=tright(i),X2[i]+h[i]-h[right(i)]+X2[i]-j • h[i]-h[right(i)]>MAX: ti,j=tleft(i),X1[i]+h[i]-h[left(i)]+j-X1[i] • h[i]-h[left(i)]  MAX && h[i]-h[right(i)]  MAX: ti,j=min(tleft(i),X1[i]+h[i]-h[left(i)]+j-X1[i], tright(i),X2[i]+h[i]-h[right(i)]+X2[i]-j ) • 状态(i, j, ti,j)的目标状态: ti,j>=0 • 将Jimmy一开始的位置记为0号平台, 初始状态: (0, X, -1) • X1[0]=X2[0]=X • h[0]=Y • 将地面作为第N+1个平台, 终态: {(N+1, j, 0): min{X1[i]: 0i<N} j max{X2[i]: 0i<N}} • 演化规则 (i, j, ti,j)(left(i), X1[i], tleft(i),X1[i])( right(i), X2[i], tright(i),X2[i])

  36. 选择合适的数据结构 • 一个数组A[1002], 每个元素代表一个高度(开始位置/各个平台/地面) • 高度h • pos[200002] • -3:未被平台覆盖 • -2:被平台覆盖 • -1:到达下一个平台的高度距离超过MAX • >=0: 安全到达地面需要的最少时间 • 按照高度对数组A排序

  37. 动态规划: 递归确定需要反演的最小状态集合 int A[1002]; int minTime(int X, int Y) { int lx, ly, rx, ry; lx = X; while ( A[Y].pos[lx-1]==-2 ) lx--; ly=Y+1; while ( A[ly].pos[lx]==-3 ) ly++; if ( A[Y].h – A[ly].h<=MAX ) { if ( A[ly].pos[lx]<-1 ) A[ly].pos[lx]=minTime(lx, ly); if (A[ly].pos[lx]==-1) A[Y].pos[X] = -1; else A[Y].pos[X] = A[ly].pos[lx] + A[Y].h – A[ly].h + X – lx; } else A[Y].pos[X] = -1; rx = X; while ( A[Y].pos[rx+1]==-2 ) rx++; ry=Y+1; while ( A[ry].pos[rx]==-3 ) ry++; if ( A[Y].h – A[ry].h>MAX ) return A[Y].pos[X] ; if ( A[ry].pos[rx]<-1 ) A[ry].pos[rx]=minTime(rx, ry); if (A[ry].pos[rx]==-1) return A[Y].pos[X] ; if ( A[Y].pos[X] > A[ry].pos[rx] + A[Y].h – A[ry].h + rx - X ) A[Y].pos[X] = A[ry].pos[rx] + A[Y].h – A[ry].h + rx - X ; return A[Y].pos[X]; }

  38. 最佳加法表达式 有一个由1..9组成的数字串.问如果将m个加号插入到这个数字串中,在各种可能形成的表达式中,值最小的那个表达式的值是多少

  39. 假定数字串长度是n,添完加号后,表达式的最后一个加号添加在第 i 个数字后面,那么整个表达式的最小值,就等于在前 i 个数字中插入 m – 1个加号所能形成的最小值,加上第 i + 1到第 n 个数字所组成的数的值。 • 状态(n, m, vn,m):前n个数字添加m个符号后的最小值为vn,m • vn,m=min{f(i)+vi,m-1: m<i<n} • f(i): 第i个数字到第n-1个数字构成的字符串的数值

  40. 作业 • ai 1088:滑雪 • ai 2774:木材加工

  41. 滑雪 • Michael喜欢滑雪百这并不奇怪, 因为滑雪的确很刺激。可是为了获得速度,滑的区域必须向下倾斜,而且当你滑到坡底,你不得不再次走上坡或者等待升降机来载你。Michael想知道载一个区域中最长的滑坡。区域由一个二维数组给出。数组的每个数字代表点的高度。下面是一个例子 • 一个人可以从某个点滑向上下左右相邻四个点之一,当且仅当高度减小。在上面的例子中,一条可滑行的滑坡为24-17-16-1。当然25-24-23-...-3-2-1更长。事实上,这是最长的一条。 • 状态(i, j, pi,j): pi,j是从(i, j)出发的最长滑坡 • pi,j= max{px,y : (x y){(i-1, j), (i+1, j), (i, j-1), (i, j+1)}&&h(x, y)<h(i,j)}+1 • 合适的数据结构: 一个二维数组A[N+2][N+2],每个元素 • h: 高度 • p: 从该点出发的最长滑坡的长度(-1, 或者>=0) • d:从该点出发的最长滑坡的走向

  42. 木材加工 • 木材厂有N根原木,它们的长度分别是一个整数. 现在想把这些木头切割成K根长度相同的小段木头.小段木头的长度要求是正整数. 请计算能够得到的小段木头的最大长度 • (N, y, lN,y ,f(lN,y)): N根原木切割成y根长度为lN,y的小段木头, f(lN,y)是每根原木的剩余部分的最大长度. 为了切割成y+1根长度相同的木头, 有两种做法 • lN,y+1=lN,y <=f(lN,y) : 如果某根原木的剩余部分长度超过lN,y, 直接从该原木上截一段下来 • lN,y+1=lN,y-x>f(lN,y) : 改变小段木头的长度, 使得在切割完第y根木头后,某根原木的剩余部分长度超过lN,y-x, 直接从该原木上截一段下来

More Related