ICode9

精准搜索请尝试: 精确搜索
首页 > 其他分享> 文章详细

Dynamic Programming

2022-02-20 23:07:36  阅读:166  来源: 互联网

标签:const int Programming Dynamic namespace max include DP


文章目录

前言

动态规划,DP(Dynamic Programming),运筹学的一个分支,同时也是求解决策过程最优化问题的过程。它和贪心一样,不是具体的某一种算法,而是一种思想

DP的学习和使用,非常地具有挑战性,会使用DP,经常可以写出令人拍案叫绝的代码。所以它也是算法竞赛,数学建模等中的一个热点问题

DP优化是对方程进行等价变换
个人博客codeslogan: dynamic programming

背包问题

状态表示与计算

f ( i , j ) f(i,j) f(i,j)存的是集合中所有选法的属性,是一个

集合划分

0-1背包

每个物品只能选一次,即放入/不放入背包,使利益最大化

01背包问题(状态转移方程讲解)深蓝

状态转移方程:

f [ i , j ] = m a x ( f [ i − 1 , j ] , f [ i − 1 , j − v ] + w ) f[i,j] = max(f[i-1,j],f[i-1,j-v]+w) f[i,j]=max(f[i−1,j],f[i−1,j−v]+w)

二维

#include <iostream>
#include <algorithm>

using namespace std;

const int N = 1010;
int n, m;
int v[N], w[N];
int f[N][N];

int main() {
    scanf("%d%d", &n, &m);
    
    for (int i = 1; i <= n; i ++ ) cin >> v[i] >> w[i];
    
    for (int i = 1; i <= n; i++) {
        for (int j = 0; j <= m; j++) {
            // 当前背包容量小于物品重量
            if (j < v[i])   f[i][j] = f[i-1][j]; // 表示不选
            else f[i][j] = max(f[i-1][j], f[i-1][j-v[i]] + w[i]); // 表示选上
        }
    }
    cout << f[n][m] << endl;
    
    return 0;
}

一维

关于内层循环要为何要逆序遍历?

如果顺序遍历,那么数组中来自上一轮的状态,会因顺序原因被污染。导致出现需要上一轮的状态时,却已经被覆盖了的情况。

换言之,如果j是顺序递增的话,则相当于f[i][j]变得是由f[i][j-v[i]]推出来的,

而不是由原来的f[i-1][j-v[i]]推的。与我们的状态转移方程不符。

#include <iostream>
#include <algorithm>

using namespace std;

const int N = 1010;
int n, m;
int v[N], w[N];
int f[N];


int main() {
    scanf("%d%d", &n, &m);
    
    for (int i = 1; i <= n; i ++ ) cin >> v[i] >> w[i];
    
    for (int i = 1; i <= n; i++) 
        for (int j = m; j >= v[i]; j--) 
            f[j] = max(f[j], f[j-v[i]] + w[i]);
    
    cout << f[m] << endl;
    
    return 0;
}

AcWing.0-1背包

完全背包

物品无限量,同一个物品可以重复放入背包中

O(n^3),朴素做法

#include <iostream>
#include <cstring>
#include <algorithm>

using namespace std;

const int N = 1010;

int n, m; // 物品数量,背包容量
int v[N], w[N]; // 体积,价值
int f[N][N];

int main() {
    cin >> n >> m;
    
    for (int i = 1; i <= n; i ++ ) cin >> v[i] >> w[i];
    
    for (int i = 1; i <= n; i ++ ) // 物品
        for (int j = 0; j <= m; j++) // 重量
            for (int k = 0; k * v[i] <= j; k++) // 物品个数
                f[i][j] = max(f[i][j], f[i-1][j-k*v[i]] + k*w[i]);
                
    cout << f[n][m] << endl;
    
    return 0;
}

O(n^2)

对k进行优化,三维降二维,状态转移方程优化推导如下:

f [ i , j ] = m a x ( f [ i − 1 , j ] , f [ i − 1 , j − v ] + w , f [ i − 1 , j − 2 v ] + 2 w , . . . ) f[i,j] = max(f[i-1,j],f[i-1,j-v]+w,f[i-1,j-2v]+2w,...) f[i,j]=max(f[i−1,j],f[i−1,j−v]+w,f[i−1,j−2v]+2w,...)

因为i-1个物品的最大值已经在上一轮循环确定了下来,所以我们只需讨论第i个物品应该选多少个

j替换为j-v

f [ i , j − v ] = m a x ( f [ i − 1 , j − v ] , f [ i − 1 , j − 2 v ] + w , f [ i − 1 , j − 3 v ] + 2 w , . . . ) f[i,j-v] = max(f[i-1,j-v],f[i-1,j-2v]+w,f[i-1,j-3v]+2w,...) f[i,j−v]=max(f[i−1,j−v],f[i−1,j−2v]+w,f[i−1,j−3v]+2w,...)

f [ i , j − v ] + w = m a x ( f [ i − 1 , j − v ] + w , f [ i − 1 , j − 2 v ] + 2 w , f [ i − 1 , j − 3 v ] + 3 w , . . . ) f[i,j-v]+w = max(f[i-1,j-v]+w,f[i-1,j-2v]+2w,f[i-1,j-3v]+3w,...) f[i,j−v]+w=max(f[i−1,j−v]+w,f[i−1,j−2v]+2w,f[i−1,j−3v]+3w,...)

将上述式子代入原方程,进行等价替换,得到状态转移方程:

f [ i , j ] = m a x ( f [ i − 1 , j ] , f [ i , j − v ] + w ) f[i,j]=max(f[i-1,j],f[i,j-v]+w) f[i,j]=max(f[i−1,j],f[i,j−v]+w)

#include <iostream>
#include <cstring>
#include <algorithm>

using namespace std;

const int N = 1010;

int n, m;
int v[N], w[N];
int f[N][N];

int main() {
    cin >> n >> m;
    
    for (int i = 1; i <= n; i ++ ) cin >> v[i] >> w[i];
    
    for (int i = 1; i <= n; i ++ )
        for (int j = 0; j <= m; j++) {
            if (j < v[i]) f[i][j] = f[i-1][j];
            else f[i][j] = max(f[i-1][j], f[i][j-v[i]] + w[i]);
        }
                
    cout << f[n][m] << endl;
    
    return 0;
}

降维

#include <iostream>
#include <cstring>
#include <algorithm>

using namespace std;

const int N = 1010;

int n, m;
int v[N], w[N];
int f[N];

int main() {
    cin >> n >> m;
    
    for (int i = 1; i <= n; i ++ ) cin >> v[i] >> w[i];
    
    for (int i = 1; i <= n; i ++ )
        for (int j = v[i]; j <= m; j++) // 从小到大循环
            f[j] = max(f[j], f[j-v[i]] + w[i]);
                
    cout << f[m] << endl;
    
    return 0;
}

同时处理输入和状态转移

#include <iostream>
#include <cstring>
#include <algorithm>

using namespace std;

const int N = 1010;

int n, m;
int f[N];

int main() {
    cin >> n >> m;
    
    for (int i = 1; i <= n; i ++ ) {
        int v, w;
        cin >> v >> w;
        for (int j = v; j <= m; j++) 
            f[j] = max(f[j], f[j-v] + w);
    }
         
    cout << f[m] << endl;
    
    return 0;
}

多重背包

每个物品的数量不同

朴素作法

#include <iostream>
#include <cstring>
#include <algorithm>

using namespace std;

const int N = 110;

int v[N], w[N], s[N];
int f[N][N];
int n, m;

int main() {
    cin >> n >> m;
    
    for (int i = 1; i <= n; i ++ ) cin >> v[i] >> w[i] >> s[i];
    
    for (int i = 1; i <= n; i ++ )
         for (int j = 0; j <= m; j++)
            for (int k = 0; k <= s[i] && k * v[i] <= j; k++)
                f[i][j] = max(f[i][j], f[i-1][j-v[i]*k] + w[i]*k);
    
    cout << f[n][m] << endl;
    
    return 0;
}

二进制优化

将一个物品的个数,表示成一段从1开始的二进制数

例如,200:1、2、4、8、16、32、64、73

为什么不选128呢?因为如果加128,可以表示的数就达到了255,超出了200。到64时,表示的数范围为127,补上一个73,就能够表示从1-200之间的任何一个数

  • 同时留意使用二进制优化时,需要开辟的数组空间为物品种数xlog2(某种物品的数量,向上取整)。因为我们在把物品数量拆成二进制表示时,要考虑到要用多少个数,才能表示出物品数量,得出上述结果
#include <iostream>
#include <algorithm>

using namespace std;

// 当有1000种物品,每种物品的数量为2000,得25000
const int N = 25000; // 留意开辟空间
int f[N];
int v[N], w[N];
int n, m;

int main() {
    cin >> n >> m;
    
    int cnt = 0;
    for (int i = 1; i <= n; i++) {
        int a, b, s;
        cin >> a >> b >> s;
        
        // 二进制优化核心代码
        int k = 1;
        while (k <= s) {
            cnt++;
            v[cnt] = a * k;
            w[cnt] = b * k;
            s -= k;
            k *= 2;
        }
        if (s > 0) {
            cnt++;
            v[cnt] = a * s;
            w[cnt] = b * s;
        }
    }
    n = cnt;
    
    // 0-1背包
    for (int i = 1; i <= n; i++) 
        for (int j = m; j >= v[i]; j--)
            f[j] = max(f[j], f[j-v[i]] + w[i]);
            
    cout << f[m] << endl;
    
    return 0;
}

分组背包

每一组中选取一样物品加入背包

#include <iostream>
#include <cstring>
#include <algorithm>

using namespace std;

const int N = 110;
int n, m;
int f[N];
int v[N][N], w[N][N], s[N];

int main() {
    cin >> n >> m;
    
    for (int i = 1; i <= n; i++) {
        cin >> s[i];
        for (int j = 0; j < s[i]; j++) {
            cin >> v[i][j] >> w[i][j];
        }
    }
    
    for (int i = 1; i <= n; i++)
        for (int j = m; j >= 0; j--)
            for (int k = 0; k < s[i]; k++)
                if (v[i][k] <= j)
                f[j] = max(f[j], f[j-v[i][k]] + w[i][k]);
    
    cout << f[m] << endl;
    
    return 0;
}

线性DP

数字三角形

DP里的Hello World!

数字三角形DP
#include <iostream>
#include <algorithm>

using namespace std;

const int N = 510, INF = 1e9;

int n;
int a[N][N], f[N][N];

int main() {
    cin >> n;
    
    for (int i = 1; i <= n; i++) 
        for (int j = 1; j <= i; j++) 
            scanf("%d", &a[i][j]);
    
    for (int i = 0; i <= n; i++) 
        for (int j = 0; j <= i + 1; j++) // 每行需要多初始化一个,考虑到两边
            f[i][j] = -INF;
    
    f[1][1] = a[1][1];
    
    for (int i = 2; i <= n; i++) // 若从1开始,结果为负无穷,由前状态转移而来
        for (int j = 1; j <= i; j++) // 从2开始才有前状态
            f[i][j] = max(f[i-1][j-1] + a[i][j], f[i-1][j] + a[i][j]);
        
    int res = -INF;
    for (int i = 1; i <= n; i++) res = max(res, f[n][i]);
        
    cout << res << endl;
    
    return 0;
}

AcWing.数字三角形

最长上升子序列

到这里为止,学习动态规划的感想是:根据题目,找到其中的一个状态,分析它是由前一个状态怎么转换得来

比方说到下图中的6这个位置上,那么要求以6结尾的最长递增子序列要通过遍历它的前六个数,判断与它们的大小关系,再加1,取最大值。而前6个数的最长又是怎么求出来的?就是根据前5个数,以此类推

image-20220220115331498 最长上升子序列DP
#include <iostream>
#include <algorithm>

using namespace std;

const int N = 1010;
int a[N], f[N], pre[N];
int n;

void PrintPath(int v) {
    if (pre[v] == 0) {
        printf("%d", a[v]);
        return;
    }
    PrintPath(pre[v]);
    printf(" %d", a[v]);
}

int main() {
    scanf("%d", &n);

    for (int i = 1; i <= n; i ++ ) scanf("%d", &a[i]);

    for (int i = 1; i <= n; i ++ ) {
        f[i] = 1; // 初始化,只有1个数以第i个数结尾
        for (int j = 1; j < i; j++) {
            if (a[j] < a[i]) {
                if (f[i] < f[j] + 1) {
                    f[i] = f[j] + 1;
                    pre[i] = j; // 记录前一个点的下标,用于还原路径
                }
            }
        }
    }

    int res = -1e9, t;
    for (int i = 1; i <= n; i ++ ) {
        if (f[i] > res) {
            res = f[i];
            t = i; // 记录最长路径的最后一个下标
        }
    }
    cout << res << endl;
    PrintPath(t);

    return 0;
}

AcWing.最长上升子序列

最长公共子序列

  • 状态表示 f [ i , j ] f[i,j] f[i,j]:s1的前i个字符和s2的前j个字符的最长公共子序列
  • 状态计算: s 1 [ i ] = s 2 [ j ] s_1[i] = s_2[j] s1​[i]=s2​[j] 和 s 1 [ i ] ! = s 2 [ j ] s_1[i] != s_2[j] s1​[i]!=s2​[j]

当两个字符相等时,容易理解,由 f [ i − 1 ] [ j − 1 ] + 1 f[i-1][j-1] + 1 f[i−1][j−1]+1转移而来

而当两个字符不等时,两个字符中一定有一个可以抛弃 f [ i − 1 ] [ j ] , f [ i ] [ j − 1 ] f[i-1][j], f[i][j-1] f[i−1][j],f[i][j−1]另

外一个可能存在于最长序列,也有可能不存在,取最大值避免了复杂的讨论情况

#include <iostream>
#include <cstring>
#include <algorithm>

using namespace std;

const int N = 1010;

int n, m;
char s1[N], s2[N];
int f[N][N];

int main() {
    cin >> n >> m >> s1 + 1 >> s2 + 1;
    
    for (int i = 1; i <= n; i ++ ) 
        for (int j = 1; j <= m; j++) {
            f[i][j] = max(f[i-1][j], f[i][j-1]);
            if (s1[i] == s2[j]) f[i][j] = max(f[i][j], f[i-1][j-1] + 1);
        }
    
    
    cout << f[n][m] << endl;
    
    return 0;
}

AcWing.最长公共子序列

区间DP

  • 状态表示 f [ i , j ] f[i,j] f[i,j]:从第i堆石子到第j堆石子合并所需的代价
  • 状态计算:将区间划分成 [ i , k ] [i,k] [i,k]和 [ k + 1 , j ] [k+1,j] [k+1,j], k = i , i + 1 , . . . , j − 1 k = i, i+1, ..., j-1 k=i,i+1,...,j−1

通过枚举区间长度,找出代价的最小值

状态转移: f [ l ] [ k ] + f [ k + 1 ] [ r ] + s [ r ] − s [ l − 1 ] f[l][k] + f[k+1][r] + s[r] - s[l-1] f[l][k]+f[k+1][r]+s[r]−s[l−1]

最后一次转移加上它的代价

#include <iostream>
#include <algorithm>

using namespace std;

const int N = 310;
int n;
int f[N][N];
int s[N];

int main() {
    scanf("%d", &n);
    
    for (int i = 1; i <= n; i ++ ) scanf("%d", &s[i]);
    
    // 前缀和,确定任一区间的权重
    for (int i = 1; i <= n; i ++ ) s[i] += s[i-1];
    
    // len=1的时候无须合并,所以从2开始
    for (int len = 2; len <= n; len++)
        // 枚举所有长度为len的情况
        for (int i = 1; i + len - 1 <= n; i++) {
            int l = i, r = i + len -1;
            f[l][r] = 1e8;
            // 在区间范围内进行划分
            for (int k = l; k <= r; k++)
                f[l][r] = min(f[l][r], f[l][k] + f[k+1][r] + s[r] - s[l-1]);
        }
    
    cout << f[1][n] << endl;
    
    return 0;
}

AcWing.石子合并

计数类DP

状态表示中属性为count的DP问题

整数划分

一个正整数n可以表示 成若干个正整数之和,形如: n = n 1 + n 2 + . . . + n k n = n_1 + n_2 + ... + n_k n=n1​+n2​+...+nk​,其中 n 1 ≥ n 2 ≥ . . . ≥ n k , k ≥ 1 n_1 \ge n_2 \ge ... \ge n_k, k \ge 1 n1​≥n2​≥...≥nk​,k≥1。

我们将这样的一种表示 称为正整数n的一种划分。

现在给定一个正整数n,请你求出n共有多少种不同的划分方法。

输入格式

共一行,包含一个整数n。

输出格式

共一行,包含一个整数,表示总划分数量。

由于答案可能 很大,输出结果请对 1 0 9 + 7 10^9+7 109+7取模。

数据范围

1 ≤ n ≤ 1000 1 \le n \le 1000 1≤n≤1000

转化为完全背包问题

  • 状态表示 f [ i , j ] f[i,j] f[i,j]:从1-i当中选,使体积恰好是j数量
  • 状态计算:针对第i个数时,有不选、选i、选2i、……、选si这些情况,容易将其转化为完全背包问题

选几个i,就要在之前的状态中提前预留出位置,因此减去

f [ i , j ] = f [ i − 1 , j ] + f [ i − 1 , j − i ] + f [ i − 1 , j − 2 i ] + . . . + f [ i − 1 , j − s i ] f[i,j] = f[i-1,j]+f[i-1,j-i]+f[i-1,j-2i]+...+f[i-1,j-si] f[i,j]=f[i−1,j]+f[i−1,j−i]+f[i−1,j−2i]+...+f[i−1,j−si]

j等价替换为j-i

​ f [ i , j − i ] = f [ i − 1 , j − i ] + f [ i − 1 , j − 2 i ] + f [ i − 1 , j − 3 i ] + . . . + f [ i − 1 , j − s i ] f[i,j-i] = f[i-1,j-i]+f[i-1,j-2i]+f[i-1,j-3i]+...+f[i-1,j-si] f[i,j−i]=f[i−1,j−i]+f[i−1,j−2i]+f[i−1,j−3i]+...+f[i−1,j−si]

代入原式,得

f [ i , j ] = f [ i − 1 , j ] + f [ i , j − i ] f[i,j]=f[i-1,j]+f[i,j-i] f[i,j]=f[i−1,j]+f[i,j−i]

#include <iostream>

using namespace std;

const int N = 1010, INF = 1e9 + 7;
int n;
int f[N];

int main() {
    cin >> n;
    
    f[0] = 1; // 一个数都不选,总和为0时的方案
    for (int i = 1; i <= n; i ++ )
        for (int j = i; j <= n; j++)
            f[j] = (f[j] + f[j-i]) % INF;
    
    cout << f[n] << endl;
    
    return 0;
}

计数类DP

  • 状态表示 f [ i , j ] f[i,j] f[i,j]:所有数的总和为i,并且表示成j个数的和的个数
  • 状态计算:将集合划分成两种情况:j个数最小值为1最小值大于1
    • 最小值为1: f [ i − 1 , j − 1 ] f[i-1,j-1] f[i−1,j−1]
    • 最小值大于1: f [ i − j , j ] f[i-j,j] f[i−j,j](每个值都减去1,一共减j,方案数不变

状态转移方程为

f [ i , j ] = f [ i − 1 , j − 1 ] + f [ i − j , j ] f[i,j]=f[i-1,j-1]+f[i-j,j] f[i,j]=f[i−1,j−1]+f[i−j,j]

最后的结果需要对式求和 f [ n ] [ i ] , i = 1 , 2 , . . . , n f[n][i],i=1, 2,...,n f[n][i],i=1,2,...,n

#include <iostream>

using namespace std;

const int N = 1010, mod = 1e9 + 7;
int n;
int f[N][N];

int main() {
    cin >> n;
    
    f[0][0] = 1;
    
    for (int i = 1; i <= n; i++)
        for (int j = 1; j <= i; j++)
            f[i][j] = (f[i-1][j-1] + f[i-j][j]) % mod;
    
    int res = 0;
    for (int i = 1; i <= n; i ++ ) res = (res + f[n][i]) % mod;
    
    cout << res << endl;
    
    return 0;
}

数位统计DP

状态压缩DP

树形DP

记忆化搜索

参考资料

AcWing算法基础课,欢迎大家报名,强推噢!!!y总带你感受算法之美!

标签:const,int,Programming,Dynamic,namespace,max,include,DP
来源: https://blog.csdn.net/comscience/article/details/123037577

本站声明: 1. iCode9 技术分享网(下文简称本站)提供的所有内容,仅供技术学习、探讨和分享;
2. 关于本站的所有留言、评论、转载及引用,纯属内容发起人的个人观点,与本站观点和立场无关;
3. 关于本站的所有言论和文字,纯属内容发起人的个人观点,与本站观点和立场无关;
4. 本站文章均是网友提供,不完全保证技术分享内容的完整性、准确性、时效性、风险性和版权归属;如您发现该文章侵犯了您的权益,可联系我们第一时间进行删除;
5. 本站为非盈利性的个人网站,所有内容不会用来进行牟利,也不会利用任何形式的广告来间接获益,纯粹是为了广大技术爱好者提供技术内容和技术思想的分享性交流网站。

专注分享技术,共同学习,共同进步。侵权联系[81616952@qq.com]

Copyright (C)ICode9.com, All Rights Reserved.

ICode9版权所有