学习利用动态规划解决若干问题
学习了一下动态规划算法,按正统的描述此类问题应该是具有“最优子结构”和“重叠子问题”的一大类问题,很多地方有非常规范的描述,这里不赘述。
简单的说,我的理解就是:
(1)大的思想是空间换时间
(2)首先分析是否“有规律”,然后定义一个状态,即为一维或二维子结点的状态,这个状态,往往是问题的解,但是也可能是隐含了解的数据,比如,求最长子串时,这个状态就是当前最长子串长度
(3)在定义状态的同时,也要分析转移方程,也叫递推公式,其实就是本结点和前面的那些已经处理过的结点之间,存在什么关系,往往是前面的结点中的最好的与当前结点存在非常特殊的关系。
(4)最后是要分析如何回溯求解,这就要配合转移方程,一般是倒序回溯,从后往前找满足转移方程的前一个结点
1 #include <stdlib.h> 2 #include <string> 3 #include <time.h> 4 5 using namespace std; 6 7 int min_of_three(int a, int b, int c) 8 { 9 if(a < b) 10 if(a < c) 11 return a; 12 else 13 return c; 14 else if(b < c) 15 return b; 16 else 17 return c; 18 } 19 20 int max_of_three(int a, int b, int c) 21 { 22 if(a > b) 23 if(a > c) 24 return a; 25 else 26 return c; 27 else if(b > c) 28 return b; 29 else 30 return c; 31 } 32 33 34 int main() 35 { 36 // ① 斐波纳契数列 37 // 问题定义:f(0) = 0; f(1) = 1; f(n) = f(n-1) + f(n-2); (n > 1),输入n,求f(n) 38 { 39 // 状态定义:a[i] 是当前f(i) 40 // 转移方程:a[i] = a[i-1] + a[i-2] 41 const int n = 7; 42 int* aux = new int[n+1]; 43 aux[0] = 0; 44 aux[1] = 1; 45 for (int i = 2; i <= n; i++) 46 aux[i] = aux[i-1] + aux[i-2]; // 根据转移方程,求出每一步的状态 47 printf(">> 斐波纳契数列问题\n"); 48 printf("n = %d\n", n); 49 printf("result = %d\n", aux[n]); 50 delete[] aux; 51 } 52 // ② 取硬币问题 53 // 问题定义:有币值为1、3、5的硬币,数量不限,输入n,求总币值为n时至少需要多少枚硬币 54 { 55 // 状态定义:a[i] 是当前币值下最小硬币数 56 // 转移方程:a[i] = 1 + min(a[i-1]:if(i>1), a[i-3]:if(i>3), a[i-5]:if(i>5)) 57 const int n = 18; 58 int* aux = new int[n+1]; 59 aux[0] = 0; 60 aux[1] = 1; 61 for (int i = 2; i <= n; i++) // 根据转移方程,求出每一步的状态 62 { 63 if(i < 3) 64 aux[i] = aux[i-1]+1; 65 else if(i < 5) 66 aux[i] = (aux[i-1]<aux[i-3]?aux[i-1]:aux[i-3])+1; 67 else 68 aux[i] = min_of_three(aux[i-1], aux[i-3], aux[i-5])+1; 69 } 70 printf(">> 取硬币问题\n"); 71 printf("n = %d\n", n); 72 printf("result = %d\n", aux[n]); 73 74 // O(n)时间找出选择的硬币 75 printf("coins are : "); 76 for (int i = n, j = n; i >= 0; i--) 77 { 78 if(aux[j] - aux[i] == 1) 79 { 80 printf("%d ", j-i); 81 j = i; 82 } 83 } 84 printf("\n"); 85 delete[] aux; 86 } 87 88 // ③ 最长递增子序列问题 89 // 问题定义:在一个字符串序列中,找出一个成递增状态的子序列,如314253,最长递增子序列为:123 90 { 91 // 状态定义:a[i] = 当前最长子序列长度 92 // 转移方程:a[i] = 1+max(a[j]:(s[j]<s[i]),j<i) 93 char* str = "AETBDTCSEEDFJEIFUGHEISDGUYUEGHSJDHDFJSDJFJKESJKJFSJFJLASDKJIWMJIFNVHISYIOSDFPEAQDMRSDYICNSHYUIYUTIIPUSDFEIVASDFWETIAZXETYLALZLQPKDPWQOWI"; 94 const int n = strlen(str); 95 int* aux = new int[n]; 96 memset(aux, 0, n*sizeof(int)); 97 for (int i = 0; i < n; i++) 98 { 99 for (int j = 0; j < i; j++) 100 if ((str[i] > str[j]) && (aux[i] < aux[j])) 101 aux[i] = aux[j]; 102 aux[i]++; 103 } 104 int len = 0; 105 for (int i = 0; i < n; i++) 106 if (aux[i] > len) 107 len = aux[i]; 108 printf(">> 最长递增子序列问题\n"); 109 printf("str = %s\n", str); 110 printf("result = %d\n", len); 111 112 // 回溯数组,找出最长子序列 113 printf("substring is : "); 114 char* sub = new char[n]; 115 memset(sub, 0, n); 116 for (int i = n-1, j = 0; i >= 0 && len; i--) 117 if (aux[i] == len && len) // 找到满足长度条件,而且递减的下一个字符,存入缓冲区 118 sub[--len] = str[i]; 119 printf("%s\n", sub); 120 delete[] aux; 121 delete[] sub; 122 } 123 // ④ 最长公共子序列问题 124 // 问题定义:两个字符串序列,找出他们公共的子序列中最长的那个,例如41253和42125,最长公共子序列为4125 125 { 126 // 状态定义:a[i][j] = str1[0:i]与str2[0:j]的最长公共子序列长度 127 // 转移方程:a[i][j] = max(a[i-1][j], a[i][j-1], a[i-1][j-1] + 1:if(str1[i] == str2[j])) 128 char* str1 = "1208347018256081230951823759028375830182837518END"; 129 char* str2 = "0827508375812398789738636472734865098127398572396239E0ERND7"; 130 const int m = strlen(str1); 131 const int n = strlen(str2); 132 int* aux = new int[m*n]; 133 memset(aux, 0, m*n*sizeof(int)); 134 for (int i = 0; i < m; i++) 135 for (int j = 0; j < n; j++) 136 { // 利用转移方程,计算每一步的最长子序列长度 137 if(i == 0 || j == 0) 138 aux[i*n+j] = str1[i]==str2[j]?1:0; 139 else 140 aux[i*n+j] = max_of_three(aux[(i-1)*n+j-1] + (str1[i]==str2[j]?1:0), aux[(i-1)*n+j], aux[i*n+j-1]); 141 } 142 printf(">> 最长公共子序列问题\n"); 143 printf("str1 = %s\nstr2 = %s\n", str1, str2); 144 printf("result = %d\n", aux[m*n-1]); 145 146 // 回溯矩阵,找出最长公共子序列 147 printf("substring is : "); 148 int len = aux[m*n-1]; 149 char* sub = new char[m]; 150 memset(sub, 0, m); 151 int i = m-1; 152 int j = n-1; 153 while (i&&j&&len) 154 { 155 while(i && (aux[(i-1)*n+j]==len)) // 找左上角,向上移动 156 i--; 157 while(j && (aux[i*n+j-1]==len)) // 找左上角,向左移动 158 j--; 159 // 到此处时,找到了左上角,锁定一个字符,该字符一定满足str1[i] == str2[j] 160 sub[--len] = str1[i]; // 因此可以不写条件:if(str1[i] == str2[j]) 161 162 i--;j--; // 向左上方移动一格 163 } 164 printf("%s\n", sub); 165 delete[] aux; 166 delete[] sub; 167 } 168 // ⑤ 格子取数问题 169 // 问题定义,有一个m列n行的格子,其中一部分格子中放有数字,只能从左上角开始,向右或下方向移动至右下角,问:如何移动才能使经过的格子中的数之和最大 170 { 171 // 状态定义:a[i][j] = 从左上角到a[i][j]的最优路径所取数字之和 172 // 转移方程:a[i][j] = max(a[i-1][j], a[i][j-1]) + num[i][j] 173 const int m = 8; 174 const int n = 10; 175 int* num = new int[m*n]; 176 int* aux = new int[m*n]; 177 srand(time(0)); 178 for (int i = 0; i < m*n; i++) 179 num[i] = (rand()%5 == 0)?rand()%4+1:0; 180 181 aux[0] = num[0]; 182 for (int i = 1; i < m; i++) 183 aux[i*n] = aux[(i-1)*n] + num[i*n]; 184 for (int j = 1; j < n; j++) 185 aux[j] = aux[j-1] + num[j]; 186 for (int i = 1; i < m; i++) 187 for (int j = 1; j < n; j++) 188 aux[i*n+j] = (aux[(i-1)*n+j]>aux[i*n+j-1]?aux[(i-1)*n+j]:aux[i*n+j-1]) + num[i*n+j]; 189 190 printf(">> 格子取数问题\n"); 191 printf("num = \n"); 192 for (int i = 0; i < m; i++) 193 { 194 for (int j = 0; j < m; j++) 195 printf(" %2d ", num[i*n+j]); 196 printf("\n"); 197 } 198 printf("\n"); 199 200 // 回溯数组,找出沿路所取的数字 201 printf("the path is : "); 202 int sum = aux[m*n-1]; 203 int* path = new int[m+n]; 204 memset(path, 0, (m+n)*sizeof(int)); 205 int i = m-1; 206 int j = n-1; 207 while (sum) 208 { 209 while(i && (aux[(i-1)*n+j]==sum)) // 找左上角,向上移动 210 i--; 211 while(j && (aux[i*n+j-1]==sum)) // 找左上角,向左移动 212 j--; 213 // 到此处时,找到了左上角,再比较哪个数最大 214 path[i+j] = num[i*n+j]; 215 sum -= num[i*n+j]; 216 if(aux[(i-1)*n+j]==sum) // 跳到前面一个最优路径上的格子里 217 i--; 218 else 219 j--; 220 } 221 for (int i = 0; i < m+n; i++) 222 if(path[i] != 0) 223 printf(" %d ", path[i]); 224 printf("\n"); 225 delete[] aux; 226 delete[] path; 227 228 229 } 230 231 system("pause"); 232 return 0; 233 }
输出结果:
>> 斐波纳契数列问题 n = 7 result = 13 >> 取硬币问题 n = 18 result = 4 coins are : 3 5 5 5 >> 最长递增子序列问题 str = AETBDTCSEEDFJEIFUGHEISDGUYUEGHSJDHDFJSDJFJKESJKJFSJFJLASDKJIWMJIFNVHISYIOSDFPEAQDMRSDYICNSHYUIYUTIIPUSDFEIVASDFWETIAZXETYLALZLQPKDPWQOWI result = 26 substring is : ABCDEFGHIJKLMNOPQRSTUVWXYZ >> 最长公共子序列问题 str1 = 1208347018256081230951823759028375830182837518END str2 = 0827508375812398789738636472734865098127398572396239E0ERND7 result = 26 substring is : 20837581239879837583823END >> 格子取数问题 num = 0 0 4 0 0 0 2 2 4 2 0 0 0 0 0 0 0 3 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 2 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0 4 2 0 0 0 0 0 1 0 1 0 3 the path is : 4 2 3 2 4 2 1 3 请按任意键继续. . .