[ACWing算法基础课]:第五章 - 动态规划

目录

  • ==背包问题 ★★★==
    • (1) 0-1 背包问题 (每样物品选1个)
    • (2)完全背包问题 (每样物品选无限个)
    • (3)多重背包问题 (每样物品限制s个)
    • (4)分组背包问题 (每组若干个, 一组只能选1个 )
  • ==线性DP ★★==
    • (1)数字三角形
    • (2)最长上升子序列 - LIS
    • (3)最长公共子序列 - LCS
    • (4)最短编辑距离

【声明】ACWing Y总课程 总结
【2023年3月23日——更新线性DP部分】

背包问题 ★★★

(1) 0-1 背包问题 (每样物品选1个)

题目介绍
[ACWing算法基础课]:第五章 - 动态规划_第1张图片
输入样例

4 5
1 2
2 4
3 4
4 5

输出

8

[版本一] 0-1背包问题的二维状态定义

  • f[i][j] : 只选前i个物品, 总体积 <= j 的 Max

状态分析

(1) 当前背包容量不够时(j < v[i]),选不了第 i 个物品,因此前i个物品的最优解和前 i - 1一样

  • f[i][j] = f[i - 1][j]

(2) 当前背包容量够第 i 个时(j ≥ v[i]),可选第 i 个物品,因此需要继续决策 选 or 不选 第i个物品

  • 选:f[i][j] = f[i - 1][j - v[i] + w[i]
  • 不选:f[i][j] = f[i - 1][j]
  • 最后取Max

状态转移方程:
f[i][j] = max(f[i-1][j], f[i-1][j-v[i]] + w[i]

C++ 代码如下

#include 
#include 
#include 
using namespace std;

const int N = 1010;
int n, m;
int v[N]; // 体积
int w[N]; // weight
int f[N][N];  // DP数组
// f[i][j] : 只选前i个物品, 总体积 <= j

int main(){
    cin >> n >> m;
    for(int i = 1; i <= n; i++) cin >> v[i] >> w[i];
    // 二维f[i][j]    
    for(int i = 1; i <= n; i++){
        for (int j = 0; j <= m; j ++ ){
            f[i][j] = f[i-1][j];  // 对应情况1
            if(j >= v[i]) // j-v[i] >= 0  对应情况2, 并决策
                f[i][j] = max(f[i][j], f[i-1][j-v[i]] + w[i]);
        } // !!! f[i][j] = max(f[i-1][j], f[i-1][j-v[i]] + w[i])
    }
    cout << f[n][m] <<endl;
    return 0;
}

[版本二——最终版本] 0-1背包问题的一维状态

  • 在分析二维状态时,第 i 轮的状态只和第 i - 1 轮有关,因此可以去掉数组第一维
  • [注] : 每轮遍历 j 的过程中, i 是不变的(都是第 i 轮的状态),
    但是当前第i轮的状态需要第 i - 1 轮的状态, 所以需要逆序枚举

举个栗子:

假如枚举到:i = 3, j = 8, v[3] = 5, w[3] = 1

二维:dp[3][8] = max(dp[2][8], dp[2][3] + w[3])   此时的dp[2][8]和dp[2][3]都是上一轮的状态值

一维:dp[8] = max(dp[8], dp[3] + w[3])      优化一维, 需要保证dp[8]和dp[3]都是上一轮的状态值

如果顺序枚举j:(第i轮) 当j = 8, j = 3 已经枚举过了,
                dp[3] 已经计算过了, 因此 dp[3] 是第i轮的状态, 而不是第i-1轮的状态  ×
                
如果逆序枚举j: (第i轮) 当j = 8, j = 3 还未枚举
                dp[3] 还未计算, 因此 dp[3] 是第i-1轮的状态  √

C++ 代码如下

#include 
#include 
#include 
using namespace std;

const int N = 1010;
int n, m;
int v[N]; // 体积
int w[N]; // weight
int f[N];  // DP数组
// f[j] :  总体积 <= j

int main(){
    cin >> n >> m;
    for(int i = 1; i <= n; i++) cin >> v[i] >> w[i];
    // 优化: 一维f[j]  v[i] <= j <= V
    for(int i = 1; i <= n; i++){
        for(int j = m; j >= v[i]; j--){ // [注] j需要逆序
            f[j] = max(f[j], f[j-v[i]] + w[i]);
        } //f[i][j] = max(f[i-1][j], f[i-1][j-v[i]] + w[i])
    }
    cout << f[m] <<endl;
    return 0;
}

(2)完全背包问题 (每样物品选无限个)

题目描述
[ACWing算法基础课]:第五章 - 动态规划_第2张图片
输入样例

4 5
1 2
2 4
3 4
4 5

输出

10

状态分析
[ACWing算法基础课]:第五章 - 动态规划_第3张图片

版本一、暴力 (TLE)

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*v[i]);
cout<< f[n][m] <<endl;

因此需要优化状态转移方程

原 : f[i , j ] = max(f[i-1,j], 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]= max(-----------f[i-1,j-v] , f[i-1,j-2*v] + w , f[i-1,j-3*v]+2*w , .....)
由上两式,可得出如下递推关系:

  • f[i][j] = max(f[i - 1][j], f[i][j - v] + w)

对比 0-1 背包的二维转移方程, 只有下标不同

  • f[i][j] = max(f[i - 1][j], f[i - 1][j - v] + w)

版本二、优化后的二维状态方程
代码如下:

for(int i = 1; i <= n; i++){
    for(int j = 0; j <= m; j++){
        // f[i][j] = max(f[i-1][j], f[i-1][j-v] + w)  0-1背包
        // f[i][j] = max(f[i-1][j], f[i][j-v] + w)   完全背包
        f[i][j] = f[i-1][j];
        if(j >= v[i]) f[i][j] = max(f[i][j], f[i][j - v[i]] + w[i]);
    }
}
cout<< f[n][m] <<endl;

版本三——最终版:一维状态方程

  • [注] : j 顺序遍历
  • 因为当前第 i 轮, f[j] 的状态只需要第 i 轮 f[j - v] 的状态

完整代码如下

#include 
#include 
#include 
using namespace std;
 
const int N = 1010;

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

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;
}

(3)多重背包问题 (每样物品限制s个)

题目描述
[ACWing算法基础课]:第五章 - 动态规划_第4张图片
输入样例

4 5
1 2 3
2 4 1
3 4 3
4 5 2

输出

10

状态分析
版本一、暴力法

  • 状态转移方程

f[i][j] = max(f[i-1][j-v[i] * k] + w[i] * k) ,k 为选的个数,范围从 0 到 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;

版本二、利用分组二进制的思想优化

  • 利用分组和二进制优化的思想, 将每个物品的数量s 分为1, 2, 4,...,2^k, c 共 log(s) + 1 组
    数量s 一定可以由二进制数表示, 保证 s == 1 + 2 + 4 + ... + 2^k + c
    [ACWing算法基础课]:第五章 - 动态规划_第5张图片

代码如下

#include 
#include 
#include 
using namespace std;
const int N = 24000, M = 2010;

int n, m;
int v[N], w[N], s[N];
int f[M];
int cnt = 0;

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 ; // V
            w[cnt] = b * k;  // W
            s -= k; // s - 2^k
            k *= 2; // k = k * 2
        }
        //剩余不满足2^k的一组
        if(s > 0) {
            cnt ++ ;
            v[cnt] = a * s; 
            w[cnt] = b * s;
        }
    }

    // for(int i = 1; i <= cnt; i++){
    //     cout << v[i] <<" "<< w[i] <
    // }
    
    // 分完组后, 每个组相当于0-1背包中的一个物品
    // 0-1背包 一维优化
    for(int i = 1; i <= cnt; 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;
}

(4)分组背包问题 (每组若干个, 一组只能选1个 )

题目描述
[ACWing算法基础课]:第五章 - 动态规划_第6张图片
输入样例

3 5
2
1 2
2 4
1
3 4
1
4 5

输出

8

状态分析
从第 i 组选一个 , 总体积不超过 j 从第 i 组选一个, 总体积不超过 j 从第i组选一个,总体积不超过j
状态转移方程 :
f[i][j] = max(f[i-1][j], f[i-1][j-v[i][k]] + w[i][k])

[注]

  • 因为第 i 轮状态依赖第 i-1 轮, 所以优化为一维的时候, j 需要逆序
  • 第 i 轮状态只依赖与 i 轮,则优化为一维的时候, j 顺序即可

代码如下

#include 
#include 
#include 
using namespace std;

const int N = 105;

int f[N]; // f[i][j] : 从第 i 组选1个, 体积不超过 j
int v[N][N], w[N][N];
int s[N]; // 组数
int n, m;

int main(){
    cin >> n >> m;
    
    for(int i = 1; i <= n; i ++){ // n : 总组数
        cin >> s[i];  // s[i] : 组内个数
        for(int j = 0; j < s[i]; j++){
            cin >> v[i][j] >> w[i][j];  // [第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 ★★

(1)数字三角形

[ACWing算法基础课]:第五章 - 动态规划_第7张图片
状态分析
f [ i ] [ j ] 表示从 ( 1 , 1 ) 到 ( i , j ) 所有方案的集合 f[i][j]表示从(1,1)到(i,j)所有方案的集合 f[i][j]表示从(1,1)(i,j)所有方案的集合
状态转移方程
f[i][j] = max(f[i-1][j-1], f[i-1][j]) + a[i][j]

[ACWing算法基础课]:第五章 - 动态规划_第8张图片

代码

#include 
#include 
#include 
#include 
using namespace std;

const int N = 510;
int f[N][N];
int a[N][N];

int main(){
    int n;
    cin>>n;
    for(int i = 1; i <= n; i++){
        for(int j = 1; j <= i; j++){
            cin >> a[i][j];
        }
    }
    for(int i = 0; i <= n; i++){
        for(int j = 0; j <= i + 1; j++){
            f[i][j] = INT_MIN;  // 初始化f数组,见图
        }
    }
    f[1][1] = a[1][1];
    for(int i = 2; i <= n; i++){
        for(int j = 1; j <= i; j++){
            f[i][j] = max(f[i-1][j-1], f[i-1][j]) + a[i][j];
        }
    }
    int ans = INT_MIN;
    for(int j = 1; j <= n; j++){
        ans = max(ans, f[n][j]);
    }
    cout << ans << endl;
    return 0;
}

(2)最长上升子序列 - LIS

状态分析
f [ i ] 表示以第 i 个数结尾的上升子序列的 m a x 长度 f[i]表示以第i个数结尾的上升子序列的max长度 f[i]表示以第i个数结尾的上升子序列的max长度
状态转移方程
f[i] = max(f[i], f[j] + 1) , j ∈ [0, i-1]

[ACWing算法基础课]:第五章 - 动态规划_第9张图片
代码

#include 
#include 
#include 
using namespace std;

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

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

LeetCode代码:LeetCode 300. 最长递增子序列

class Solution {
public:
    int lengthOfLIS(vector<int>& nums) {
        int n = nums.size();
        int f[n+1];
        for(int i = 0; i < n; i++){
            f[i] = 1;
            for(int j = 0; j < i; j++){
                if(nums[j] < nums[i]) f[i] = max(f[i], f[j] + 1);
            }
        }
        int ans = 0;
        for(int i = 0; i < n; i++){
            ans = max(ans, f[i]);
            // cout << f[i] << " " << ans << endl;
        }
        return ans;
    }
};

(3)最长公共子序列 - LCS

推荐一个讲得比较好的视频 最长公共子序列LCS
状态分析
f [ i ] [ j ] 表示 : s 1 [ 0 − i ] 和 s 2 [ 0 − j ] 的 m a x 公共子序列长度 f[i][j]表示: s_1[0-i] 和s_2[0-j]的max公共子序列长度 f[i][j]表示:s1[0i]s2[0j]max公共子序列长度

图源上述视频

  • 状态转移,分2种情况讨论
  • 1.s1[i] == s2[j]
  • 2.s1[i] != s2[j]
    [ACWing算法基础课]:第五章 - 动态规划_第10张图片
    [ACWing算法基础课]:第五章 - 动态规划_第11张图片
    [ACWing算法基础课]:第五章 - 动态规划_第12张图片

状态转移方程
s1[i] == s2[j], dp[i][j] = dp[i-1][j-1] + 1
s1[i] != s2[j], dp[i][j] = max(dp[i-1][j], dp[i][j-1])

代码:

#include 
#include 
#include 

using namespace std;

const int N = 1050;
int n, m;
char a[N], b[N];
int f[N][N];


int main(){
    cin >> n >> m;
    cin >> a + 1 >> b + 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(a[i] == b[j]){
                f[i][j] = max(f[i][j], f[i-1][j-1] + 1);
            }
        }
    }
    cout << f[n][m] << endl;
    return 0;
}

LeetCode代码:1143. 最长公共子序列

注意:下标从0开始

class Solution {
public:
    int longestCommonSubsequence(string t1, string t2) {
        int n = t1.size(), m = t2.size();
        int f[n+1][m+1];
        memset(f, 0, sizeof(f));
        for(int i = 0; i < n; i++){
            for(int j = 0; j < m; j++){
                if(t1[i] == t2[j]){
                    f[i+1][j+1] = f[i][j] + 1;
                }else 
                    f[i+1][j+1] = max(f[i][j+1], f[i+1][j]);
            }
        }
        return f[n][m];
    }
};

空间优化:滚动数组(2个一维)

class Solution {
public:
    int longestCommonSubsequence(string t1, string t2) {
        int n = t1.size(), m = t2.size();
        int f[2][m+1];
        memset(f, 0, sizeof(f));
        for(int i = 0; i < n; i++){
            for(int j = 0; j < m; j++){
                if(t1[i] == t2[j]){
                    f[(i+1)%2][j+1] = f[i%2][j] + 1;
                }else 
                	f[(i+1)%2][j+1] = max(f[i%2][j+1], f[(i+1)%2][j]);
            }
        }
        return f[n%2][m];
    }
};

空间优化:一维数组
【注意】在使用一维数组的情况下,由于f[i+1][j+1]是由f[i][j]f[i+1][j]f[i][j+1](左上、左、上)转移过来的,所以需要用临时变量t来保存左上状态(f[i][j]),否则f[i][j]会被f[i+1][j]覆盖。

[ACWing算法基础课]:第五章 - 动态规划_第13张图片

class Solution {
public:
    int longestCommonSubsequence(string t1, string t2) {
        int n = t1.size(), m = t2.size();
        int f[m+1];
        memset(f, 0, sizeof(f));
        for(int i = 0; i < n; i++){
            int pre_i_j = 0;
            for(int j = 0; j < m; j++){
                int t = f[j+1];
                if(t1[i] == t2[j]){
                    f[j+1] = pre_i_j + 1;
                }else 
                    f[j+1] = max(f[j+1], f[j]);
                pre_i_j = t;
            }
        }
        return f[m];
    }
};\

(4)最短编辑距离

[ACWing算法基础课]:第五章 - 动态规划_第14张图片
状态转移方程
s[i] == t[j], dp[i][j] = dp[i-1][j-1]
s[i] != t[j], dp[i][j] = min(dp[i][j-1], dp[i-1][j], dp[i-1][j-1]) + 1

  1. 插入:s插入一个字符 等价于 当前操作次数=前i个字符转为t前j-1字符的次数 + 1
  2. 删除:s删除一个字符 等价于 当前操作次数=前i-1个字符转为t前j个字符的次数 + 1
  3. 更改:s更改一个字符 等价于 当前操作次数=前i-1个字符转为t前j-1个字符的次数 + 1

代码

class Solution {
public:
    int minDistance(string s, string t) {
        int n1 = s.size(), n2 = t.size();
        int dp[n1+1][n2+1];
        memset(dp, 0, sizeof(dp));
        // dp[i][j]:从 [长度为i的字符串s] 转换为 [长度为j的字符串t] 的最小操作次数
        // 边界条件1:当 len(s) = 0, len(t) = j时, dp[0][j] = j;
        // 边界条件2:当 len(s) = i, len(t) = 0时, dp[i][0] = i; 
        for(int j = 0; j <= n2; j++) dp[0][j] = j;
        for(int i = 0; i <= n1; i++) dp[i][0] = i;
        for(int i = 0; i < n1; i++){
            for(int j = 0; j < n2; j++){
                if(s[i] == t[j]){
                    dp[i+1][j+1] = dp[i][j];  // 无需操作
                }else{ // 插入 || 删除 || 更改  + 1   (s插入一个字符 等价于 t删除一个字符)
                    dp[i+1][j+1] = min({dp[i+1][j], dp[i][j+1], dp[i][j]}) + 1;
                }
            }
        }
        return dp[n1][n2];
    }
};

你可能感兴趣的:(ACWing算法基础,动态规划,算法,贪心算法,c++)