概念

Dynamic Programming is a technique that combines the correctness of complete search and the efficiency of greedy algorithms. Dynamic Programming can be applied if the problem can be divided into overlapping subproblems that can be solved independently.There are two uses for Dynamic Programming:

  • Finding an optimal solution: We want to find a solution that is as large as possible or as small as possible.
  • Counting the number of solutions: We want to calculate the total number of possible solutions.

Understanding dynamic programming is a milestone in every competitive programmer’s career. While the basic idea is simple, the challenge is how to apply dynamic programming to different problems.

The dynamic programming algorithm is based on a recursive function that goes through all possibilities how to form the sum, like a brute force algorithm. However, the dynamic programming algorithm is efficient because it uses memoization and calculates the answer to each subproblem only once.

Recursive formulation,再看硬币问题

贪心算法的策略是每次取最大值,但并不是每一次都有效。我们下面看看,

反例, 背包体积是10,有三个物品 体积9,价值100 体积3,价值60 体积7,价值70

如果是贪心,优先选择价值100的,占用9体积,就不能选其他了 如果是dp,选择体积是3+7的方案,获得价值130

image.png
image.png
image.png
image.png

  1. // 代码如下,但这个写法不是很高效(加上记忆化就好了)
  2. int solve(int x) {
  3. if (x < 0) return INF; //INF代表一个无穷大的值
  4. if (x == 0) return 0;
  5. int best = INF;
  6. for (auto c : coins){ //枚举可以使用的硬币 这是C++11的写法
  7. best = min(best, solve(x-c)+1);
  8. }
  9. return best;
  10. }

Using memoization,记忆化

image.png

  1. //下面就是加记忆化的写法
  2. //The time complexity of the algorithm is O(nk),
  3. //where n is the target sum and k is the number of coins.
  4. //题目背景,凑够n元,最少需要几枚硬币
  5. int solve(int x) {
  6. if (x < 0) return INF;
  7. if (x == 0) return 0;
  8. if (ready[x]) return value[x];
  9. int best = INF;
  10. for (auto c : coins) {
  11. best = min(best, solve(x-c)+1);
  12. }
  13. value[x] = best;
  14. ready[x] = true;
  15. return best;
  16. }
  17. //这份代码,并不是模板,理解明白就行

image.png

  1. // 下面是循环的写法
  2. value[0] = 0;
  3. for (int x = 1; x <= n; x++) { //枚举sum
  4. value[x] = INF;
  5. for (auto c : coins) { //枚举硬币
  6. if (x-c >= 0) {
  7. value[x] = min(value[x], value[x-c]+1);
  8. }
  9. }
  10. }
  11. // 这份代码,并不是模板,理解明白就行
  12. // value[n]

image.png

关于记忆化的补充:

When we solve a recursive problem that its sub-problems overlaps(重叠), hence calling sub-problems. More than once and repeating its calculation in nature that typically makes the order exponentials!

When the original space is small enough to be memorized, then saving these sub-problems makes order small too, as sub-problems calculated once.

  1. // Fibonacci Series:
  2. // Fib(n) = fib(n-1) + fib(n-2).
  3. // Fib(0) = fib(1) = 1
  4. int fib(int n)
  5. {
  6. if(n <= 1)
  7. return 1;
  8. return fib(n-2) + fib(n-1);
  9. }

Let’s SAVE the answer, and let space of N is called 2N times!

  1. int savedAnswers[MAX]; // Initialized to -1, means no answer
  2. int fibSave(int n)
  3. {
  4. if(n <= 1)
  5. return 1;
  6. if(savedAnswers[n] != -1)
  7. return savedAnswers[n];
  8. return savedAnswers[n] = fib(n-2) + fib(n-1);
  9. // O(N) Search space, hence O(N) memory -> O(N) Order!

General Rules:
1- Recursive Function
2- Sub-calls Overlap
3- Small Search Space, so putting in memory is doable

加记忆化的例子【Luogu P5017】Noip2018-T3 摆渡车题解

接下来加记忆化。其实在写记忆化搜索的时候,我个人认为加记忆化是最简单的,只要爆搜写好并且写对,加记忆化易如反掌。哪里有 return ,哪里加记忆化。最后代码如下(由于其余部分相同,这里只给出 dfs 代码)

加记忆化的例子聊聊动态规划与记忆化搜索【特别经典】

  • 不依赖任何 外部变量
  • 答案以返回值的形式存在, 而不能以参数的形式存在(就是不能将 dfs 定义成 dfs(pos ,tleft , nowans ), 这里面的 nowans 不符合要求).
  • 对于相同一组参数, dfs 返回值总是相同的

加记忆化方法总结:

  1. 写出这道题的暴搜程序(最好是dfs)
  2. 将这个dfs改成”无需外部变量”的dfs
  3. 添加记忆化数组

DP need good recursive mentality, that views answer in term of recursion! So develop your skills! Many DPs follow a certain pattern (style), we will investigate some of them.

分治与dp的区别

Do we need to apply DP for merge sort? NEVER, a call will never be repeated! like most of Divide-and-Conquer Algorithm.

分治与dp,两者都是将问题划分为子问题,然后求解子问题。
当子问题不会被求解多次的时候,应该用分治;被求解多次的,就dp。
例如,二叉查找,用分治;求fibo,用dp。

Dynamic Programming most typical cases: Minimization, Maximization and Counting. But could have adhock usages. In fact, above code is not called DP, it is call Memoization (NOT Memorization). It is a technique when we have a recusive function and save calls).

DP is to build bottom up according to recurrence while Memoization is top-down.

  1. int dp_fib(int n)
  2. {
  3. int fib[MAX];
  4. fib[0] = fib[1] = 1; // base case
  5. for(int i = 2; i <= n; ++i)
  6. fib[i] = fib[i-1] + fib[i-2]; // bottom up according to recurrence
  7. return fib[n];
  8. }

Writing Memoization is much more natural, although there are cases when DP is a must.

01背包问题(递归)

  1. // 问题描述:
  2. // 重量,W: 10, 4 , 20, 5, 7
  3. // 价值,P: 10, 15, 3 , 1, 4
  4. // knapsack size = 12
  5. // 最佳方案:0 1 0 0 1
  6. const int MAX = 5;
  7. int n = 5;
  8. int weights[MAX] = {10, 4, 20, 5, 7};
  9. int benfit[MAX] = {10, 15, 3, 1, 4};
  10. // called with knapsack(0, intialWeight)
  11. int knapsack(int i, int reminder) // aka(also known as) 0/1 knapsack
  12. {
  13. if(i == n)
  14. return 0;
  15. int choice1 = knapsack(i+1, reminder);
  16. int choice2 = 0;
  17. if(reminder >= weights[i])
  18. choice2 = knapsack(i+1, reminder - weights[i]) + benfit[i];
  19. return max(choice1, choice2);
  20. }
  21. //这个版本还需要加上记忆化,待完善

Constructing a solution打印方案(循环,递归)

image.png
dp问题当中,有很多情况,不仅让你求最值,还让你输出最值情况下的方案是什么(如果有多种方案,输出一种即可,oj会做special judge)

  1. //we can declare another array that indicates for each sum of money
  2. //the first coin in an optimal solution:
  3. value[0] = 0;
  4. for (int x = 1; x <= n; x++) { //枚举体积
  5. value[x] = INF;
  6. for (auto c : coins) { //枚举硬币
  7. if (x-c >= 0 && value[x-c]+1 < value[x]) {
  8. value[x] = value[x-c]+1;
  9. first[x] = c;
  10. }
  11. }
  12. }
  13. //输出方案
  14. while (n > 0) {
  15. cout << first[n] << "\n";
  16. n -= first[n];
  17. }
  1. #include <iostream>
  2. using namespace std;
  3. int n, m, a[15];
  4. int value[15];
  5. int first[15];
  6. int main()
  7. {
  8. memset(value, 0x3f, sizeof value);
  9. cin >> n >> m;
  10. for (int i = 1; i <= n; i++) cin >> a[i];
  11. value[0] = 0;
  12. for (int x = 1; x <= m; x++){ //枚举体积
  13. for (int i = 1; i <= n; i++) //枚举硬币
  14. if (x - a[i] >= 0 && value[x - a[i]] + 1 < value[x]){
  15. value[x] = value[x - a[i]] + 1;
  16. first[x] = a[i]; //记录方案
  17. }
  18. }
  19. int sum = m;
  20. while (sum > 0){
  21. cout << first[sum] << ' ';
  22. sum -= first[sum];
  23. }
  24. puts("");
  25. return 0;
  26. }
  27. /*
  28. 3 10
  29. 1 3 4
  30. */

输出方案,我们更多的,还可以用递归输出

  1. void print(int x)
  2. {
  3. if (x == 0) return ;
  4. printf("%d ", first[x]); //这两行的顺序,决定了先递归再输出当前,还是先输出当前,再递归
  5. print(x - first[x]);
  6. }
  7. int main()
  8. {
  9. //..........
  10. print(10);
  11. puts("");
  12. return 0;
  13. }
  1. #include <iostream>
  2. using namespace std;
  3. int n, m, a[15];
  4. int value[15];
  5. int first[15];
  6. void print(int x)
  7. {
  8. if (x == 0) return ;
  9. printf("%d ", first[x]);
  10. print(x - first[x]);
  11. }
  12. int main()
  13. {
  14. memset(value, 0x3f, sizeof value);
  15. cin >> n >> m;
  16. for (int i = 1; i <= n; i++) cin >> a[i];
  17. value[0] = 0;
  18. for (int x = 1; x <= m; x++){ //枚举体积
  19. for (int i = 1; i <= n; i++) //枚举硬币
  20. if (x - a[i] >= 0 && value[x - a[i]] + 1 < value[x]){
  21. value[x] = value[x - a[i]] + 1;
  22. first[x] = a[i]; //记录方案
  23. }
  24. }
  25. print(m);
  26. puts("");
  27. return 0;
  28. }
  29. /*
  30. 3 10
  31. 1 3 4
  32. */

Counting the number of solutions统计方案个数

image.png
image.png

  1. count[0] = 1;
  2. for (int x = 1; x <= n; x++) { //枚举sum
  3. for (auto c : coins) { //枚举硬币
  4. if (x-c >= 0) {
  5. count[x] += count[x-c];
  6. //count[x] %= m;
  7. }
  8. }
  9. }

image.png

  1. #include <iostream>
  2. using namespace std;
  3. int n, m, a[15];
  4. int value[15];
  5. int cnt[15];
  6. int main()
  7. {
  8. memset(value, 0x3f, sizeof value);
  9. cin >> n >> m;
  10. for (int i = 1; i <= n; i++) cin >> a[i];
  11. cnt[0] = 1; //体积为0有一种方案
  12. for (int x = 1; x <= m; x++){ //枚举sum,从小到大枚举
  13. for (int i = 1; i <= n; i++) //枚举硬币
  14. if (x - a[i] >= 0){
  15. cnt[x] += cnt[x - a[i]];
  16. }
  17. }
  18. cout << cnt[m] << '\n';
  19. return 0;
  20. }
  21. /*
  22. 输入
  23. 3 5
  24. 1 3 4
  25. 输出
  26. 6
  27. */

— Now we have discussed all basic ideas of dynamic programming.

更多的示例

  1. // 记忆化更多的例子
  2. // Let's move to some other examples! Remember:
  3. // Given grid of positive numbers, Start from 0, 0 and end at n, n. Move only to right and down - find path with sum of numbers is maximum.
  4. /*
  5. 15
  6. 24
  7. 512
  8. 678
  9. 149
  10. What paths there" 51289, 51789, 51749, 56789, 56749, 56149 ..
  11. It is like: 5 { 1 {289, 789, 749}, 6{789, 749, 149} }
  12. In other words, 5 needs answer of 1 and of 6 to maximize over them.
  13. */
  14. int grid[MAX][MAX];
  15. // Think in function F(i, j) that find solution from (i, j) to (n, n)
  16. int maxPathSum(int r, int c)
  17. {
  18. if( !valid(r, c))
  19. return 0;
  20. if (r == n-1 && c == n-1)
  21. return grid[r][c]; // base
  22. int path1 = maxPathSum(r, c+1); // right
  23. int path2 = maxPathSum(r+1, c); // down
  24. return grid[r][c] + max(path1, path2);
  25. }
  26. // How to trun code to memization?
  27. // 1- Create array of input dimensions, and output of its return.
  28. // E.g. int mem[MAX][MAX];
  29. // Initialize it with a value that will never be a correct answer, e.g. -1
  30. // If value is -1, then it is not visited before. Else, use saved value
  31. int mem[MAX][MAX]; // R & C is in range N. Function return int
  32. int maxPathSum_save(int r, int c)
  33. {
  34. //1- Always hanle invalid calls first
  35. if( !valid(r, c))
  36. return 0;
  37. //2- Handle Base cases
  38. if (r == n-1 && c == n-1)
  39. return grid[r][c]; // base
  40. //3- check if visited before
  41. if(mem[r][c] != -1)
  42. return mem[r][c];
  43. int path1 = maxPathSum_save(r, c+1); // right
  44. int path2 = maxPathSum_save(r+1, c); // down
  45. return mem[r][c] = grid[r][c] + max(path1, path2);
  46. }
  1. /*
  2. Consider an arbitrary sequence of integers. One can place + or - operators between integers in the sequence,
  3. thus deriving different arithmetical expressions that evaluate to different values. Let us, for example,
  4. take the sequence: 17, 5, -21, 15. There are eight possible expressions:
  5. 17 + 5 + -21 + 15 = 16
  6. 17 + 5 + -21 - 15 = -14
  7. 17 + 5 - -21 + 15 = 58
  8. 17 + 5 - -21 - 15 = 28
  9. 17 - 5 + -21 + 15 = 6
  10. 17 - 5 + -21 - 15 = -24
  11. 17 - 5 - -21 + 15 = 48
  12. 17 - 5 - -21 - 15 = 18
  13. We call the sequence of integers divisible by K if + or - operators can be placed between integers in the
  14. sequence in such way that resulting value is divisible by K. In the above example, the sequence is divisible by 7 (17+5+-21-15=-14) but is not divisible by 5.
  15. You are to write a program that will determine divisibility of sequence of integers.
  16. */
  17. // called with tryAll1(1, v[0]) // e.g. tryAll1(1, 17)
  18. int tryAll1(int i, int sum) {
  19. if (i == n)
  20. return sum % k == 0;
  21. if (tryAll1(i + 1, sum + v[i]) || tryAll1(i + 1, sum - v[i]) )
  22. return 1;
  23. return 0;
  24. }
  25. /////////////////////////////////////////////////
  26. int fix(int a) {
  27. return (a % k + k) % k;
  28. }
  29. int tryAll2(int i, int mod) {
  30. int &ret = mem[i][mod];
  31. if (ret != -1)
  32. return ret;
  33. if (i == n)
  34. return ret = mod == 0;
  35. if (tryAll2(i + 1, fix(mod + v[i])) || tryAll2(i + 1, fix(mod - v[i])))
  36. return ret = 1;
  37. return ret = 0;
  38. }
  1. TC: RGBStreet
  2. http://community.topcoder.com/stat?c=problem_statement&pm=6680
  3. const int MAX = 21;
  4. int r[MAX];
  5. int g[MAX];
  6. int b[MAX];
  7. int n;
  8. const int OO = (int)1e6;
  9. int mem[MAX][4];
  10. int minCost(int i, int lasColor)
  11. {
  12. if(i == n)
  13. return 0;
  14. int &ret = mem[i][lasColor]; //注意这个写法
  15. if(ret != -1)
  16. return ret;
  17. ret = OO;
  18. if(lasColor != 0)
  19. ret = min(ret, r[i] + minCost(i+1, 0));
  20. if(lasColor != 1)
  21. ret = min(ret, g[i] + minCost(i+1, 1));
  22. if(lasColor != 2)
  23. ret = min(ret, b[i] + minCost(i+1, 2));
  24. return ret;
  25. }
  26. class RGBStreet {
  27. public:
  28. int estimateCost(vector <string> houses)
  29. {
  30. rep(i, houses)
  31. {
  32. istringstream iss(houses[i]);
  33. iss>>r[i]>>g[i]>>b[i];
  34. }
  35. n = sz(houses);
  36. clr(mem, -1);
  37. return minCost(0, 3);
  38. }
  39. };

《一本通》题目

【例9.2】数字金字塔
  1. //数字三角形模型

【例9.3】求最长不下降序列
  1. //LIS

【例9.4】拦截导弹(Noip1999)
  1. //LIS

【例9.5】城市交通路网
  1. //LIS

【例9.6】挖地雷
  1. //LIS

【例9.7】友好城市
  1. //LIS

【例9.8】合唱队形
  1. //LIS

【例9.9】最长公共子序列
  1. //LCS

【例9.10】机器分配
  1. //dp[i][j]前i个公司,分配j台设备的最大价值
  2. //递归打印方案,这个输出方案的方案,需要多学习

最长上升子序列
  1. //LIS

最大子矩阵
  1. //最大子段和 + 前缀和,O(n^3)

登山
  1. //LIS

摘花生
  1. //数字三角形模型

最大上升子序列和
  1. //LIS

怪盗基德的滑翔翼
  1. //LIS

最低通行费
  1. //LIS

三角形最佳路径问题
  1. //数字三角形模型

拦截导弹
  1. //LIS
  2. //如果要拦截所有导弹最少要配备多少套这种导弹拦截系统

【例9.11】01背包问题
  1. //01背包

【例9.12】完全背包问题
  1. //完全背包

【例9.13】庆功会
  1. //多重背包

【例9.14】混合背包
  1. //混合背包

【例9.15】潜水员
  1. //这个题目注意一个瓶子里的氧气和氮气,不是完整的用,可以只用一部分从而满足工作需要。

https://www.acwing.com/solution/content/7438/

背包问题中 体积至多是 j ,恰好是 j ,至少是 j 的初始化问题的研究

【例9.16】分组背包
  1. //分组背包,每组里只能选一个物品

【例9.17】货币系统
  1. //完全背包,求方案数

采药
  1. //01背包,noip原题

数字组合
  1. //01背包,求方案数

宠物小精灵之收服
  1. //小智的精灵球数量和皮卡丘的初始体力
  2. //二维费用背包
  3. //输出,收服C个小精灵时皮卡丘的剩余体力值最多为R。这个最后输出剩余体力值的方法也很聪明

买书
  1. //完全背包,求方案数
  2. //和 数字组合 ,比较像

Charm Bracelet
  1. //01背包

装箱问题
  1. //任取若干个装入箱内,使箱子的剩余空间为最小
  2. //01背包,输出V-dp[V]

开餐馆
  1. //LIS模型,i接在1..i-1谁后面的时候,注意判断距离问题

【例9.18】合并石子
  1. //区间dp

【例9.19】乘积最大
  1. //int dp[N][M]; //前i个数,插入了j个乘号
  2. //先预处理出来数字的问题,更好一些

https://www.acwing.com/solution/content/18940/

【例9.20】编辑距离
  1. //编辑距离,经典问题

【例9.21】方格取数
  1. //从左上走到右下,走了两次,问能取到的最大值
  2. //定义一个四维的状态,dp[N][N][N][N],经过发现,可以优化到三维

【例9.22】复制书稿(book)
  1. //输出具体方案,用了一个贪心的策略去设计输出答案的过程
  2. //以下两种设计都可行,请体验体验
  3. //设计状态dp[i][j] i个人,抄前j本书的复制时间
  4. //设计状态dp[i][j] i个本书,有前j个人去抄

【例9.23】橱窗布置(flower)
  1. //dp[i][j] 前i朵花插在前j个花瓶里

【例9.24】滑雪
  1. //记忆化

公共子序列
  1. //LCS

计算字符串距离
  1. //编辑距离

糖果
  1. //《1195:判断整除》这道递推类似,请回忆。

鸡蛋的硬度
  1. //特别经典的一道题目,很难的,基本自己想不出来的,可以思考二十分钟试一试
  2. //有两种设计状态的方法
  3. //dp[i][j] 测量长度i层楼,用j个鸡蛋,最坏情况下需要扔的次数,O(n^2*m)
  4. //dp[i][j] 用j个鸡蛋测i次,最多能测量的区间长度,O(nm)

大盗阿福
  1. //从一个商店,可以被盗,也可以不被盗的两种情况入手,思考状态的设计dp[N][2]
  2. //还有一种思路,线性dp的思路,状态只有一维,类似最大字段和的O(n)操作

股票买卖
  1. //dp,状态机模型
  2. //ybt的第8个测试点,跑起来不稳定,有可能会TLE这个点
  3. //dp[N][3][2]; //dp(i,j,0)第i天,买卖j次,当前没有股票;dp(i,j,1)当前有股票

鸣人的影分身
  1. //分苹果->鸣人的影分身->数的划分
  2. //本题可以从分苹果入手,实现记忆化搜索的版本
  3. //然后,再改成循环版本
  4. //最后,再重新的,从集合角度出发,分析 (最小值为0 | 最小值不为0) 的推导过程

数的划分
  1. //先从鸣人的影分身,变形一下,实现第一个版本,trick一下就可以
  2. //重新的,从集合角度出发,分析(.... | .....)

Maximum sum
  1. //这段题目求的是,两个不重合的最大子段和
  2. //最大子段和,这个会写的
  3. //如何表示两个不重合的最大子段和呢? 多画图看一看就会找到线索

最长公共子上升序列
  1. //LIS+LCS,非常好的题目,这个题面还要求输出方案。
  2. //有O(n^4), O(n^3), O(n^2)的写法,都需要深入思考,琢磨实现