420 likes | 619 Views
程序设计实习. 第八讲 动态规划. 动态规划与搜索. 当我们面临的问题是 , 寻找一个操作序列 , 将初态演化成目标状态 . 计算解空间的元素时 , 决定计算效率的关键因素是所产生的状态数量 判定重复的状态 判定是否能导出目标状态 搜索 : 从初态出发 , 演化出目标状态 演化规则 : 状态之间的变换关系 , S S 1 ||…|| S S k 对状态的处理 : 有序的使用各备选操作 , 分别实施状态变换产生一个新的状态 动态规划 : 从终态出发 , 反演出目标状态 演化规则 : 状态之间的依赖关系 , S S 1 …S k
E N D
程序设计实习 第八讲 动态规划
动态规划与搜索 • 当我们面临的问题是, 寻找一个操作序列,将初态演化成目标状态. 计算解空间的元素时, 决定计算效率的关键因素是所产生的状态数量 • 判定重复的状态 • 判定是否能导出目标状态 • 搜索: 从初态出发, 演化出目标状态 • 演化规则: 状态之间的变换关系, SS1||…||SSk • 对状态的处理: 有序的使用各备选操作, 分别实施状态变换产生一个新的状态 • 动态规划: 从终态出发, 反演出目标状态 • 演化规则: 状态之间的依赖关系, SS1…Sk • 对状态的处理: • 使用演化操作, 产生一组新的状态 • “迭加”各新状态的目标状态, 实现状态变换 • 终态与目标状态 • 终态: 不能从该状态演化出其它的状态 • 目标状态: 符合条件的状态
例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)
动态规划: 递归确定需要反演的最小状态集合 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]; }
动态规划: 按照状态演化规则有序反演新的状态 终态: (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]; }
例2POJ1163数字三角形 7 3 8 8 1 0 2 7 4 4 4 5 2 6 5 在上面的数字三角形中寻找一条从顶部到底边的路径,使得路径上所经过的数字之和最大。路径上的每一步都只能往左下或右下走。只需要求出这个最大和即可,不必给出具体路径。 三角形的行数大于1小于等于100,数字为 0 - 99
输入格式: • //三角形行数。下面是三角形 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)
动态规划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]; }
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; }
动态规划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; }
动态规划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]; }
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; }
动态规划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]; }
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; }
数字三角型:四种解法的开销比较 7 3 8 8 1 0 2 7 4 4 4 5 2 6 5 • 递归反演1: • 递归调用 • 判定重复状态时使用二维数组 • 递归反演2: • 递归调用 • 无重复状态判断:时间复杂度为 2n,对于 n = 100,肯定超时 • 按规则反演1: • 判定重复状态时使用二维数组 • 按规则反演2 : • 判定重复状态时使用一维数组 • 在这个问题中, 按规则反演隐含了重复状态判断
例题:最长上升子序列 问题描述 一个数的序列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). 你的任务,就是对于给定的序列,求出最长上升子序列的长度。
输入数据 输入的第一行是序列的长度N (1 <= N <= 1000)。第二行给出序列中的N个整数,这些整数的取值范围都在0到10000。 输出要求 最长上升子序列的长度。 输入样例 7 1 7 3 5 9 4 8 输出样例 4
问题分析 • 记目标序列为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)
动态规划: 递归确定需要反演的最小状态集合 #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)); }
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; }
动态规划 :按照演化规则有序反演新的状态 #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)); }
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; }
例题: Poj 1458 最长公共子序列 给出两个字符串,求出这样的一个最长的公共子序列的长度:子序列中的每个字符都能在两个原串中找到,而且每个字符的先后顺序和原串中的先后顺序一致。
Sample Input abcfbc abfcab programming contest abcd mnp Sample Output 4 2 0 最长公共子序列
问题分析 • (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)
动态规划: 递归确定需要反演的最小状态集合 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]; }
动态规划:按照演化规则有序反演新的状态 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]; }
两种动态规划实现方法的比较 • 递归确定需要反演的最小状态集合 • 所产生的每个状态需要一次递归调用 • 所产生的每个状态都是实施初态的状态变换时必须的 • 按照演化规则有序反演新的状态 • 所产生的部分状态对目标状态没有贡献 • 是否用递归函数实现动态规划? 关键在于是否有利于减少所反演的新状态的数量
参考实现代码 #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;
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; } }
例题: POJ 1661 Help Jimmy "Help Jimmy" 是在下图所示的场景上完成的游戏:
场景中包括多个长度和高度各不相同的平台。地面是最低的平台,高度为零,长度无限。 Jimmy老鼠在时刻0从高于所有平台的某处开始下落,它的下落速度始终为1米/秒。当Jimmy落到某个平台上时,游戏者选择让它向左还是向右跑,它跑动的速度也是1米/秒。当Jimmy跑到平台的边缘时,开始继续下落。Jimmy每次下落的高度不能超过MAX米,不然就会摔死,游戏也会结束。 设计一个程序,计算Jimmy到地面时可能的最早时间。
输入数据 第一行是测试数据的组数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一定能安全到达地面。
输出要求 对输入的每组测试数据,输出一个整数,Jimmy到地面时可能的最早时间。 输入样例 1 3 8 17 20 0 10 8 0 10 13 4 14 3 输出样例 23
问题分析 • 状态(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]: 0i<N} j max{X2[i]: 0i<N}} • 演化规则 (i, j, ti,j)(left(i), X1[i], tleft(i),X1[i])( right(i), X2[i], tright(i),X2[i])
选择合适的数据结构 • 一个数组A[1002], 每个元素代表一个高度(开始位置/各个平台/地面) • 高度h • pos[200002] • -3:未被平台覆盖 • -2:被平台覆盖 • -1:到达下一个平台的高度距离超过MAX • >=0: 安全到达地面需要的最少时间 • 按照高度对数组A排序
动态规划: 递归确定需要反演的最小状态集合 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]; }
最佳加法表达式 有一个由1..9组成的数字串.问如果将m个加号插入到这个数字串中,在各种可能形成的表达式中,值最小的那个表达式的值是多少
假定数字串长度是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个数字构成的字符串的数值
作业 • ai 1088:滑雪 • ai 2774:木材加工
滑雪 • 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:从该点出发的最长滑坡的走向
木材加工 • 木材厂有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, 直接从该原木上截一段下来