算法篇:动态规划II

发布时间:2024年01月11日

35、647. 回文子串
①状态表示:dp[i][j]表示:s字符串[i,j]的子串,是否是回文串。(i<=j)
②状态转移方程:
if(s[i]!=s[j]) dp[i][j]=false;
else if(s[i]==s[j])
{
?? ?if(i==j||i+1==j)dp[i][j]=true;
?? ?else dp[i+1][j-1];
}
③初始化:无需初始化。
④填表顺序:从下往上。
⑤返回值:dp表中true的个数。

class Solution 
{
public:
    int countSubstrings(string s) 
    {
        int n = s.size();
        vector<vector<bool>> dp(n, vector<bool>(n));
        int ret = 0;
        for(int i = n - 1; i >= 0; i--)
        {
            for(int j = i; j < n; j++)
            {
                if(s[i] == s[j]) 
                    dp[i][j] = i + 1 < j ? dp[i + 1][j - 1] : true;
                if(dp[i][j])
                    ret++;
            }
        }
        return ret;
    }
};

36、5. 最长回文子串
①状态表示:dp[i][j]表示:s字符串[i,j]的子串,是否是回文串。(i<=j)
②状态转移方程:
if(s[i]!=s[j]) dp[i][j]=false;
else if(s[i]==s[j])
{
?? ?if(i==j||i+1==j)dp[i][j]=true;
?? ?else dp[i+1][j-1];
}
③初始化:无需初始化。
④填表顺序:从下往上。
⑤返回值:dp表中true的情况下,长度最大的子串的起始位置以及长度。

class Solution 
{
public:
    string longestPalindrome(string s) 
    {
        int n = s.size();
        vector<vector<bool>> dp(n, vector<bool>(n));
        int len = 1, begin = 0;
        for(int i = n - 1; i >= 0; i--)
        {
            for(int j = i; j < n; j++)
            {
                if(s[i] == s[j]) 
                    dp[i][j] = i + 1 < j ? dp[i + 1][j - 1] : true;
                if(dp[i][j] && j - i + 1 > len)
                {
                    len = j - i + 1 , begin = i;
                }
            }
        }
        return s.substr(begin, len);
    }
};

37、1745. 分割回文串 IV

class Solution 
{
public:
    bool checkPartitioning(string s) 
    {
        // 1、用dp把所有子串是否是回文预处理一下
        int n = s.size();
        vector<vector<bool>> dp(n, vector<bool>(n));
        int len = 1, begin = 0;
        for(int i = n - 1; i >= 0; i--)
            for(int j = i; j < n; j++)
                if(s[i] == s[j]) 
                    dp[i][j] = i + 1 < j ? dp[i + 1][j - 1] : true;

        // 2、枚举所有的第二个字符串的起始位置以及结束位置
        for(int i = 1; i < n - 1; i++)
            for(int j = i; j < n - 1; j++)
                if(dp[0][i - 1] && dp[i][j] && dp[j + 1][n - 1])
                    return true;
        return false;
    }
};

38、132. 分割回文串 II
①状态表示:dp[i]表示:s[0,i]区间上的的子串,最少分割次数。
②状态转移方程:
0~i是回文串:0
0~i不是回文串:(0<j<=i)j~i是否回文:
若是:dp[i]=min(dp[j-1]+1) ?若否:×?
优化:二维dp表,将所有的子串是否是回文的信息,保存在dp表里面。
③初始化:dp表内所有值都初始为无穷大。
④填表顺序:从左往右填表。
⑤返回值:dp[n-1]。

class Solution 
{
public:
    int minCut(string s) 
    {
        // 1、用dp把所有子串是否是回文预处理一下
        int n = s.size();
        vector<vector<bool>> isPal(n, vector<bool>(n));
        for(int i = n - 1; i >= 0; i--)
            for(int j = i; j < n; j++)
                if(s[i] == s[j]) 
                    isPal[i][j] = i + 1 < j ? isPal[i + 1][j - 1] : true;

        // 2、
        vector<int> dp(n, INT_MAX);
        for(int i = 0; i < n; i++)
        {
            if(isPal[0][i]) dp[i] = 0;
            else
            {
                for(int j = 1; j <= i; j++)
                {
                    if(isPal[j][i]) dp[i]=min(dp[i], dp[j-1]+1);
                }
            }
        }
        return dp[n - 1];
    }
};

39、516. 最长回文子序列
①状态表示:
dp[i]表示:以i位置为结尾的所有子序列中,最长的回文子序列的长度。
dp[i][j]表示:s字符串[i,j]区间内的所有子序列中,最长的回文子序列的长度。
②状态转移方程:
s[i]==s[j]:if(i==j) dp[i][j]=1
?? ? ? else if(i+1==j) dp[i][j]=2
?? ? ? else dp[i][j]=dp[i+1][j-1]+2
s[i]!=s[j]:dp[i][j]=max(dp[i][j-1],dp[i+1][j])
③初始化:无需初始化。
④填表顺序:从下往上填写,每一行从左往右填表。
⑤返回值:dp[0][n-1]。

class Solution 
{
public:
    int longestPalindromeSubseq(string s) 
    {
        int n = s.size();
        vector<vector<int>> dp(n, vector<int>(n));
        for(int i = n - 1; i >= 0; i--)
        {
            dp[i][i] = 1;
            for(int j = i + 1; j < n; j++)
            {
                if(s[i] == s[j]) dp[i][j] = dp[i + 1][j - 1] + 2;
                else dp[i][j] = max(dp[i][j - 1], dp[i + 1][j]);
            }
        }
        return dp[0][n - 1];
    }
};

40、1312. 让字符串成为回文串的最少插入次数
①状态表示:
dp[i][j]表示:s字符串[i,j]区间内的所有子序列中,使它成为回文串的最小插入次数。
②状态转移方程:
s[i]==s[j]:if(i==j) dp[i][j]=0
???????else if(i+1==j) dp[i][j]=0
???????else dp[i][j]=dp[i+1][j-1]
s[i]!=s[j]:dp[i][j]=min(dp[i][j-1],dp[i+1][j])+1
③初始化:无需初始化。
④填表顺序:从下往上填写,每一行从左往右填表。
⑤返回值:dp[0][n-1]。

class Solution 
{
public:
    int minInsertions(string s) 
    {
        int n = s.size();
        vector<vector<int>> dp(n, vector<int>(n));
        for(int i = n - 1; i >= 0; i--)
        {
            for(int j = i + 1; j < n; j++)
            {
                if(s[i] == s[j]) dp[i][j] = dp[i + 1][j - 1];
                else dp[i][j] = min(dp[i][j - 1], dp[i + 1][j]) + 1;
            }
        }
        return dp[0][n - 1];
    }
};

41、1143. 最长公共子序列
①状态表示:
dp[i][j]表示:s1的[0,i]区间以及s2的[0,j]区间内的所有子序列中,最长公共子序列的长度。
②状态转移方程:根据最后一个位置的状况,分情况讨论。
s[i]==s[j]:dp[i][j]=dp[i-1][j-1]+1
s[i]!=s[j]:dp[i][j]=max(dp[i][j-1],dp[i-1][j])
③初始化:关于字符串的dp问题:空串是有研究意义的。引入空串的概念后,会方便我们初始化。
(1)里面的值要保证我们后续的填表是正确的·。
(2)下标的映射关系:下标减1或s1=“ ”+s1 ?s2=“ ”+s2
④填表顺序:从上往下,每一行从左往右填表。
⑤返回值:dp[m][n]。

class Solution 
{
public:
    int longestCommonSubsequence(string s1, string s2) 
    {
        int m = s1.size(), n = s2.size();
        s1 = " " + s1, s2 = " " + s2;
        vector<vector<int>> dp(m + 1, vector<int>(n + 1));
        for(int i = 1; i <= m; i++)
        {
            for(int j = 1; j <= n; j++)
            {
                if(s1[i] == s2[j]) dp[i][j] = dp[i - 1][j - 1] + 1;
                else dp[i][j] = max(dp[i][j - 1],dp[i - 1][j]);
            }
        }
        return dp[m][n];
    }
};

43、115. 不同的子序列
①状态表示:
dp[i][j]表示:s字符串[0,j]区间内所有的子序列,有多少个t字符串[0,i]区间内的子串。
②状态转移方程:根据s的子序列的最后一个位置包不包含s[j]
包含s[j]:t[i]==s[j]:dp[i][j]=dp[i-1][j-1]
不包含s[j]:dp[i][j]=dp[i][j-1]
dp[i][j]=dp[i][j-1]+dp[i-1][j-1]
③初始化:引入空串。
(1)里面的值要保证我们后续的填表是正确的。
(2)下标的映射关系:下标减1或s=“ ”+s
④填表顺序:从上往下,每一行从左往右填表。
⑤返回值:dp[m][n]。

class Solution 
{
public:
    int numDistinct(string s, string t) 
    {
        int m = t.size(), n = s.size();
        vector<vector<double>> dp(m + 1, vector<double>(n + 1));
        for(int j = 0; j <= n; j++) dp[0][j] = 1; // 初始化
        for(int i = 1; i <= m; i++)
            for(int j = 1; j <= n; j++)
            {
                dp[i][j] += dp[i][j - 1];
                if(t[i - 1] == s[j - 1]) dp[i][j] += dp[i - 1][j - 1];
            }
        return dp[m][n];
    }
};

44、115. 不同的子序列
①状态表示:
dp[i][j]表示:p[0,j]区间内的子串能否匹配s[0,i]区间内的子串。
②状态转移方程:根据最后一个位置的状况,分情况讨论。
p[j]是普通字符→if(s[i]==p[i]&&dp[i-1][j-1]==true) dp[i][j]=true
p[j]=='?'→if(dp[i-1][j-1]==true) dp[i][j]=true
p[j]=='*'→代替n个字符:dp[i][j]=dp[i-n][j-1]
优化:
法1:数学:
dp[i][j]=dp[i][j-1]||dp[i-1][j-1]||dp[i-2][j-1]......=dp[i][j-1]||dp[i-1][j]
法2:根据状态表示以及实际情况,优化状态转移方程
空串→dp[i][j-1]
匹配一个,但不舍去→dp[i][j]=dp[i-1][j]→dp[i-2][j]→dp[i-3][j]......
dp[i][j]=dp[i][j-1]||dp[i-1][j]
③初始化:引入空串。
(1)里面的值要保证我们后续的填表是正确的。
(2)下标的映射关系:下标减1或s=“ ”+s
④填表顺序:从上往下,每一行从左往右填表。
⑤返回值:dp[m][n]。

class Solution 
{
public:
    bool isMatch(string s, string p) 
    {
        int m = s.size(), n = p.size();
        s = " " + s, p = " " + p;
        vector<vector<bool>> dp(m + 1, vector<bool>(n + 1));

        // 初始化
        dp[0][0] = true;
        for(int j = 1; j <= n; j++)
        {
            if(p[j] == '*') dp[0][j] = true;
            else break;
        }

        for(int i = 1; i <= m; i++)
        {
            for(int j = 1; j <= n; j++)
            {
                if(p[j] == '*') dp[i][j] = dp[i][j - 1] || dp[i - 1][j];
                else dp[i][j] = (p[j] == '?' || s[i] == p[j]) && dp[i - 1][j - 1];
            }
        }
        return dp[m][n];
    }
};

45、10. 正则表达式匹配
①状态表示:
dp[i][j]表示:p[0,j]区间内的子串能否匹配s[0,i]区间内的子串。
②状态转移方程:根据最后一个位置的状况,分情况讨论。
p[j]是普通字符→if(s[i]==p[i]&&dp[i-1][j-1]==true) dp[i][j]=true
p[j]=='.'→if(dp[i-1][j-1]==true) dp[i][j]=true
p[j]=='*'→p[j-1]=='.'→代替n个'.'→dp[i][j]=dp[i-n][j-2]
?? ?→p[j-1]是普通字符→空串→dp[i][j-2]
?? ??? ??? ?→匹配一个,然后保留→p[j-1]=s[i]&&dp[i-1][j]

总结:
dp[i][j]=(s[i]==p[i]||p[j]=='.')&&dp[i-1][j-1]
dp[i][j]=dp[i][j-2]||(p[j-1]=='.'||p[j-1]==s[i])&&dp[i-1][j]

优化:
法1:数学:
p[j]=='*'→dp[i][j]=dp[i][j-2]||dp[i-1][j-2]||dp[i-2][j-2]......=dp[i][j-2]||dp[i-1][j]
法2:根据状态表示以及实际情况,优化状态转移方程
p[j]=='*'→".*"干掉一个之后保留→dp[i][j]=dp[i-1][j]
?? ?→".*"舍去→dp[i][j-2]

③初始化:引入空串。
(1)里面的值要保证我们后续的填表是正确的。
(2)下标的映射关系:下标减1或s=“ ”+s
④填表顺序:从上往下,每一行从左往右填表。
⑤返回值:dp[m][n]。

class Solution 
{
public:
    bool isMatch(string s, string p) 
    {
        int m = s.size(), n = p.size();
        s = " " + s, p = " " + p;
        vector<vector<bool>> dp(m + 1, vector<bool>(n + 1));

        // 初始化
        dp[0][0] = true;
        for(int j = 2; j <= n; j+=2)
        {
            if(p[j] == '*') dp[0][j] = true;
            else break;
        }

        for(int i = 1; i <= m; i++)
        {
            for(int j = 1; j <= n; j++)
            {
                if(p[j] == '*') dp[i][j] = dp[i][j-2] || (p[j - 1] == '.' || p[j - 1] == s[i]) && dp[i - 1][j];
                else dp[i][j] = (s[i] == p[j] || p[j] == '.') && dp[i - 1][j - 1];
            }
        }
        return dp[m][n];
    }
};

46、97. 交错字符串
①状态表示:
dp[i][j]表示:s1中[1,i]内的字符串以及[1,j]区间内的字符串,能否拼凑成s3[1,i+j]区间内的字符串。
②状态转移方程:根据最后一个位置的状况,分情况讨论。
if(s1[i]==s3[i+j]&&dp[i-1][j]==true) dp[i][j]=true
if(s2[j]==s3[i+j]&&dp[i][j-1]==true) dp[i][j]=true
③初始化:略。
④填表顺序:从上往下,每一行从左往右填表。
⑤返回值:dp[m][n]。

class Solution 
{
public:
    bool isInterleave(string s1, string s2, string s3) 
    {
        int m = s1.size(), n = s2.size();
        if(m + n != s3.size()) return false; 
        s1 = " " + s1, s2 = " " + s2, s3 = " " + s3;
        vector<vector<bool>> dp(m + 1, vector<bool>(n + 1));

        // 初始化
        dp[0][0] = true;
        for(int j = 1; j <= n; j++) // 初始化第一行
        {
            if(s2[j] == s3[j]) dp[0][j] = true;
            else break;
        }

        for(int i = 1; i <= m; i++) // 初始化第一列
        {
            if(s1[i] == s3[i]) dp[i][0] = true;
            else break;
        }

        for(int i = 1; i <= m; i++)
        {
            for(int j = 1; j <= n; j++)
            {
                dp[i][j] = (s1[i] == s3[i + j] && dp[i - 1][j] == true) 
                        || (s2[j] == s3[i + j] && dp[i][j - 1]);
            }
        }
        return dp[m][n];
    }
};

47、712. 两个字符串的最小ASCII删除和
①状态表示:求两个字符串里面所欲的公共子序列里面,ASCLL的最大和。
dp[i][j]表示:s1中[0,i]区间以及s2的[0,j]区间内的所有子序列里,公共子序列的ASCll最大和。
②状态转移方程:根据最后一个位置的状况,分情况讨论。
有s1[i],有s2[j]→s1[i]==s2[j]→dp[i][j]=dp[i-1][j-1]+s1[i]
有s1[i],没有s2[j]→dp[i][j]=dp[i][j-1]
没有s1[i],有s2[j]→dp[i][j]=dp[i-1][j]
没有s1[i],没有s2[j]→dp[i][j]=dp[i-1][j-1]×
③初始化:一行一列虚拟节点。
④填表顺序:从上往下,每一行从左往右填表。
⑤返回值:sum-2*dp[m][n]。

class Solution 
{
public:
    int minimumDeleteSum(string s1, string s2) 
    {
        int m = s1.size(), n = s2.size();
        vector<vector<int>> dp(m + 1, vector<int>(n + 1));

        for(int i = 1; i <= m; i++)
            for(int j = 1; j <= n; j++)
            {
                dp[i][j] = max(dp[i][j - 1], dp[i - 1][j]);
                if(s1[i - 1] == s2[j - 1])
                    dp[i][j] = max(dp[i][j], dp[i - 1][j - 1] + s1[i - 1]);
            }

        int sum = 0;
        for(auto s : s1) sum += s;
        for(auto s : s2) sum += s;
        return sum - 2 * dp[m][n];
    }
};

49、牛客 DP41 【模板】01背包
①状态表示:
dp[i]表示:从前i个物品中选,所有的选法中,能挑选出来的最大价值。×
dp[i][j]表示:从前i个物品中选,总体积不超过j,所有的选法中,能挑选出来的最大价值。
②状态转移方程:根据最后一步的状况,分情况讨论。
不选i物品→dp[i][j]=dp[i-1][j-1]
选i物品→dp[i][j]=dp[i-1][j-v[i]]+w[i]
③初始化:一行一列虚拟节点。
④填表顺序:从上往下。
⑤返回值:dp[n][v]。

#include <iostream>
#include <string.h>
using namespace std;

const int N = 1001;

int n, V, v[N], w[N];
int dp[N][N];

int main() 
{
    // 读入数据
    cin >> n >> V;
    for(int i = 1; i <= n; i++)
        cin >> v[i] >> w[i];

    // 解决第一问
    for(int i = 1; i <= n; i++)
        for(int j = 1; j <= V; j++)
        {
            dp[i][j] = dp[i - 1][j];
            if(j >= v[i]) dp[i][j] = max(dp[i][j],dp[i - 1][j - v[i]] + w[i]);
        }
    cout << dp[n][V] << endl;

    // 解决第二问
    memset(dp, 0, sizeof dp);
    for(int j = 1; j <= V; j++) dp[0][j] = -1;
    for(int i = 1; i <= n; i++)
        for(int j = 1; j <= V; j++)
        {
            dp[i][j] = dp[i - 1][j];
            if(j >= v[i] && dp[i -1][j - v[i]] != -1) 
                dp[i][j] = max(dp[i][j],dp[i - 1][j - v[i]] + w[i]);
        }
    cout << (dp[n][V] == -1 ? 0 : dp[n][V]) << endl;

    return 0;
}

优化:
①利用滚动数组做空间上的优化
②直接在原始的代码上稍加修改即可:删除所有的横坐标并修改j的遍历顺序。
不选i物品→dp[j]=dp[j]
选i物品→dp[i][j]=dp[j-v[i]](遍历顺序→从右往左)

#include <iostream>
#include <string.h>
using namespace std;

const int N = 1001;

int n, V, v[N], w[N];
int dp[N];

int main() 
{
    // 读入数据
    cin >> n >> V;
    for(int i = 1; i <= n; i++)
        cin >> v[i] >> w[i];

    // 解决第一问
    for(int i = 1; i <= n; i++)
        for(int j = V; j >= v[i]; j--) // 修改遍历顺序
            dp[j] = max(dp[j],dp[j - v[i]] + w[i]);
    cout << dp[V] << endl;

    // 解决第二问
    memset(dp, 0, sizeof dp);
    for(int j = 1; j <= V; j++) dp[j] = -1;
    for(int i = 1; i <= n; i++)
        for(int j = V; j >= v[i]; j--)
            if(dp[j - v[i]] != -1) 
                dp[j] = max(dp[j],dp[j - v[i]] + w[i]);
    cout << (dp[V] == -1 ? 0 : dp[V]) << endl;

    return 0;
}

50、416.分割等和子集
①状态表示:(在数组中选择一些数出来,让这些数的和等于sum/2)
dp[i][j]表示:从前i个数中选,所有的选法中,能否凑成j这个数。
②状态转移方程:根据最后一步的状况,分情况讨论。
不选i→dp[i][j]=dp[i-1][j]
选i(j>=nums[i])→dp[i][j]=dp[i-1][j-nums[i]]
dp[i][j]=dp[i-1][j]||dp[i-1][j-nums[i]]
③初始化:一行一列虚拟节点。
④填表顺序:从上往下。
⑤返回值:dp[n][sum/2]。

class Solution 
{
public:
    bool canPartition(vector<int>& nums) 
    {
        int n = nums.size(), sum = 0;
        for(auto x : nums) sum += x;
        if(sum % 2) return false;

        int aim = sum / 2;
        vector<vector<bool>> dp(n + 1, vector<bool>(aim + 1));

        for(int i = 0; i <= n; i++) dp[i][0] = true;
        for(int i = 1; i <= n; i++)
            for(int j = 1; j <= aim; j++)
            {
                dp[i][j] = dp[i-1][j];
                if(j >= nums[i - 1]) dp[i][j] = dp[i][j] || dp[i - 1][j - nums[i - 1]];
            }
            return dp[n][aim];
    }
};

优化:

class Solution 
{
public:
    bool canPartition(vector<int>& nums) 
    {
        int n = nums.size(), sum = 0;
        for(auto x : nums) sum += x;
        if(sum % 2) return false;

        int aim = sum / 2;
        vector<bool> dp(aim + 1);

        dp[0] = true;
        for(int i = 1; i <= n; i++)
            for(int j = aim; j >= nums[i - 1]; j--)
                dp[j] = dp[j] || dp[j - nums[i - 1]];
            return dp[aim];
    }
};

51、494. 目标和
①状态表示:(转化:在数组中选择一些数,让这些数的和等于(target+sum)/2,问有多少种选法)
dp[i][j]表示:从前i个数中选,总和正好等于j,一共有多少种选法。
②状态转移方程:根据最后一步的状况,分情况讨论。
不选i→dp[i][j]=dp[i-1][j]
选i(j>=nums[i])→dp[i][j]=dp[i-1][j-nums[i]]
dp[i][j]=dp[i-1][j]+dp[i-1][j-nums[i]]
③初始化:一行一列虚拟节点,且初始化第一行即可。
④填表顺序:从上往下。
⑤返回值:dp[n][target+sum)/2]。

class Solution 
{
    int aim, ret;
public:
    int findTargetSumWays(vector<int>& nums, int target) 
    {
        int n = nums.size(), sum = 0;
        for(auto x : nums) sum += x;
        int aim = (target + sum) / 2;
        if(aim < 0 || (sum + target) % 2) return 0;
        
        vector<vector<int>> dp(n + 1, vector<int>(aim + 1));

        dp[0][0] = 1;
        for(int i = 1; i <= n; i++)
            for(int j = 0; j <= aim; j++)
            {
                dp[i][j]=dp[i - 1][j];
                if(j >= nums[i - 1]) dp[i][j] += dp[i - 1][j - nums[i - 1]];
            }
        return dp[n][aim];
    }
};

⑥优化

class Solution 
{
    int aim, ret;
public:
    int findTargetSumWays(vector<int>& nums, int target) 
    {
        int n = nums.size(), sum = 0;
        for(auto x : nums) sum += x;
        int aim = (target + sum) / 2;
        if(aim < 0 || (sum + target) % 2) return 0;
        
        vector<int> dp(aim + 1);

        dp[0] = 1;
        for(int i = 1; i <= n; i++)
            for(int j = aim; j >= nums[i - 1] && j >= 0; j--)
                dp[j] += dp[j - nums[i - 1]];
        return dp[aim];
    }
};

52、1049. 最后一块石头的重量 II
①状态表示:(转化:在数组中选择一些数,让这些数的和等于尽可能接近sum/2,问有多少种选法)
dp[i][j]表示:从前i个数中选,总和不超过i,此时的最大和。
②状态转移方程:根据最后一步的状况,分情况讨论。
不选i→dp[i][j]=dp[i-1][j]
选i(j>=nums[i])→dp[i][j]=dp[i-1][j-nums[i]]+nums[i]
dp[i][j]=max(dp[i-1][j],dp[i-1][j-nums[i]]+nums[i])
③初始化:一行一列虚拟节点,且初始化第一行即可。
④填表顺序:从上往下填写每一行。
⑤返回值:sum-2*dp[n][sum/2]。

class Solution 
{
public:
    int lastStoneWeightII(vector<int>& stones) 
    {
        int sum = 0;
        for(auto x : stones) sum += x;
        int n = stones.size(), m = sum / 2;
        vector<vector<int>> dp(n + 1, vector<int>(m + 1));

        for(int i = 1; i <= n; i++)
            for(int j = 0; j <= m; j++)
            {
                dp[i][j]=dp[i - 1][j];
                if(j >= stones[i - 1]) dp[i][j] = max(dp[i][j], dp[i - 1][j - stones[i - 1]] + stones[i - 1]);
            }
        return sum - 2 * dp[n][sum / 2];
    }
};

优化:

class Solution 
{
public:
    int lastStoneWeightII(vector<int>& stones) 
    {
        int sum = 0;
        for(auto x : stones) sum += x;
        int n = stones.size(), m = sum / 2;
        vector<int> dp(m + 1);

        for(int i = 1; i <= n; i++)
            for(int j = m; j >= 0; j--)
                if(j >= stones[i - 1]) dp[j] = max(dp[j], dp[j - stones[i - 1]] + stones[i - 1]);
        return sum - 2 * dp[m];
    }
};

53、牛客.DP42?【模板】完全背包
(1)①状态表示:
dp[i][j]表示:从前i个物品中选,总体积不超过j,所有的选法中,最大的价值。
②状态转移方程:根据最后一步的状况,分情况讨论。
不选dp[i][j]=dp[i-1][j]
选1个dp[i][j]=dp[i-1][j-v[i]]+w[i]
选2个dp[i][j]=dp[i-1][j-2*v[i]]+2*w[i]
......
优化:
dp[i][j]=max(dp[i-1][j],dp[i-1][j-v[i]]+w[i],dp[i-1][j-2*v[i]]+2*w[i]......dp[i-1][j-kv[i]]+kw[i])
dp[i][j-v[i]]=max(dp[i-1][j-v[i]],dp[i-1][j-2*v[i]]+w[i]......dp[i-1][j-kv[i]]+xw[i])+(x-1)w[i]
(其中k=x)
故dp[i][j]=dp[i][j-v[i]]+w[i]
综上:dp[i][j]=max(dp[i-1][j],dp[i][j-v[i]] +w[i])
③初始化:一行一列虚拟节点,且初始化第一行即可。
④填表顺序:从上往下,每一行从左往右填写每一行。
⑤返回值:dp[n][v]。

(2)①状态表示:
dp[i][j]表示:从前i个物品中选,总体积等于j,所有的选法中,最大的价值。
②状态转移方程:用-1表示没有这个状态
dp[i][j]=max(dp[i-1][j],dp[i][j-v[i]] +w[i]) (dp[i][j-v[i]]!=-1)
③初始化:一行一列虚拟节点,且初始化第一行即可。(除第一个位置,都初始为-1)
④填表顺序:从上往下,每一行从左往右填写每一行。
⑤返回值:dp[n][v]==-1?0:dp[n][v]。

#include <iostream>
#include <string.h>
using namespace std;

const int N = 1010;

int n, V, v[N], w[N];
int dp[N][N];

int main() 
{
    // 读入数据
    cin >> n >> V;
    for (int i = 1; i <= n; i++)
        cin >> v[i] >> w[i];

    for (int i = 1; i <= n; i++)
        for (int j = 0; j <= V; j++) 
        {
            dp[i][j] = dp[i - 1][j];
            if (j >= v[i]) 
                dp[i][j] = max(dp[i][j], dp[i][j - v[i]] + w[i]);
        }
    cout << dp[n][V] << endl;

    memset(dp, 0, sizeof dp);
    for (int j = 1; j <= V; j++) dp[0][j] = -1;
    for (int i = 1; i <= n; i++)
        for (int j = 0; j <= V; j++) 
        {
            dp[i][j] = dp[i - 1][j];
            if (j >= v[i] && dp[i][j - v[i]] != -1) 
                dp[i][j] = max(dp[i][j], dp[i][j - v[i]] + w[i]);
        }
    cout << (dp[n][V] == -1 ? 0 : dp[n][V]) << endl;

    return 0;
}

优化:利用滚动数组,做空间上的优化。
状态转移方程:dp[j]=max(dp[j],dp[j-v[i]]+w[i])?
填表顺序:从左往右遍历(和01背包进行对比)

#include <iostream>
#include <string.h>
using namespace std;

const int N = 1010;

int n, V, v[N], w[N];
int dp[N];

int main() 
{
    // 读入数据
    cin >> n >> V;
    for (int i = 1; i <= n; i++)
        cin >> v[i] >> w[i];

    for (int i = 1; i <= n; i++)
        for (int j = 0; j <= V; j++) 
            if (j >= v[i]) 
                dp[j] = max(dp[j], dp[j - v[i]] + w[i]);
    cout << dp[V] << endl;

    memset(dp, 0, sizeof dp);
    for (int j = 1; j <= V; j++) dp[j] = -1;
    for (int i = 1; i <= n; i++)
        for (int j = v[i]; j <= V; j++) 
            if (dp[j - v[i]] != -1) 
                dp[j] = max(dp[j], dp[j - v[i]] + w[i]);
    cout << (dp[V] == -1 ? 0 : dp[V]) << endl;

    return 0;
}

54、322. 零钱兑换
①状态表示:
dp[i][j]表示:从前i个硬币中选,总和正好等于j,所有的选法中,最少的硬币个数。
②状态转移方程:根据最后一步的状况,分情况讨论。
不选i→dp[i][j]=dp[i-1][j]
选1个i→dp[i][j]=dp[i-1][j-coins[i]]+1
选2个i→dp[i][j]=dp[i-1][j-2*coins[i]]+2
......
dp[i][j]=min(dp[i-1][j],dp[i-1][j-coins[i]]+1) (j-coins[i]>=0)
③初始化:一行一列虚拟节点,且初始化第一行,除了第一个位置,都初始化为0x3f3f3f3f。
④填表顺序:从上往下,每一行从左往右填写。
⑤返回值:dp[n][amount]>=INF?-1:dp[n][amount]。

class Solution 
{
public:
    int coinChange(vector<int>& coins, int amount) 
    {
        const int INF = 0x3f3f3f3f;

        int n = coins.size();
        vector<vector<int>> dp(n + 1, vector<int>(amount + 1));
        for(int j = 1; j <= amount; j++)
            dp[0][j] = INF;
        for(int i = 1; i <= n; i++)
            for(int j = 0; j <= amount; j++)
            {
                dp[i][j] = dp[i-1][j];
                if(j >= coins[i - 1])
                    dp[i][j] = min(dp[i][j], dp[i][j - coins[i - 1]] + 1);
            }
        return dp[n][amount] >= INF ? -1 : dp[n][amount];
    }
};

优化:

class Solution 
{
public:
    int coinChange(vector<int>& coins, int amount) 
    {
        const int INF = 0x3f3f3f3f;

        int n = coins.size();
        vector<int> dp(amount + 1, INF);
        dp[0] = 0;
        for(int i = 1; i <= n; i++)
            for(int j = coins[i - 1]; j <= amount; j++)
                    dp[j] = min(dp[j], dp[j - coins[i - 1]] + 1);
        return dp[amount] >= INF ? -1 : dp[amount];
    }
};

55、518. 零钱兑换 II
①状态表示:
dp[i][j]表示:从前i个硬币中选,总和正好等于j,一共有多少种选法。
②状态转移方程:根据最后一步的状况,分情况讨论。
不选i→dp[i][j]=dp[i-1][j]
选1个i→dp[i][j]=dp[i-1][j-coins[i]]
选2个i→dp[i][j]=dp[i-1][j-2*coins[i]]
......
dp[i][j]=min(dp[i-1][j],dp[i-1][j-coins[i]]) (j-coins[i]>=0)
③初始化:一行一列虚拟节点,且初始化第一行,除了第一个位置初始化为1,都初始化为0。
④填表顺序:从上往下,每一行从左往右填写。
⑤返回值:dp[n][amount]]。

class Solution 
{
public:
    int change(int amount, vector<int>& coins) 
    {
        int n = coins.size();
        vector<int> dp(amount + 1);
        dp[0] = 1;
        for(int i = 1; i <= n; i++)
            for(int j = 0; j <= amount; j++)
                if(j >= coins[i - 1])
                    dp[j] += dp[j - coins[i - 1]];
        return dp[amount];
    }
};

56、279. 完全平方数
①状态表示:
dp[i][j]表示:从前i个完全平方数中挑选,总和正好等于j,所有的选法中,最小的数量。
②状态转移方程:根据最后一步的状况,分情况讨论。
不选i方→dp[i][j]=dp[i-1][j]
选1个i方→dp[i][j]=dp[i-1][j-i方]+1
选2个i方→dp[i][j]=dp[i-1][j-2*i方]+2
......
dp[i][j]=min(dp[i-1][j],dp[i][j-i方]+1)
③初始化:一行一列虚拟节点,且初始化第一行,除了第一个位置初始化为0,都初始化为无穷大。
④填表顺序:从上往下,每一行从左往右填写。
⑤返回值:dp[n开方][n]。

class Solution 
{
public:
    int numSquares(int n) 
    {
        int m = sqrt(n);
        vector<vector<int>> dp(m + 1, vector<int>(n + 1));
        for(int j = 1; j <= n; j++) dp[0][j] = 0x3f3f3f3f;
        for(int i = 1; i <= m; i++)
            for(int j = 0; j <= n; j++)
            {
                dp[i][j] =  dp[i - 1][j];
                if(j >= i * i)
                    dp[i][j] = min(dp[i][j], dp[i][j - i * i] + 1);
            }
        return dp[m][n];
    }
};

优化:

class Solution 
{
public:
    int numSquares(int n) 
    {
        int m = sqrt(n);
        vector<int> dp(n + 1);
        for(int j = 1; j <= n; j++) dp[j] = 0x3f3f3f3f;
        for(int i = 1; i <= m; i++)
            for(int j = i * i; j <= n; j++)
                dp[j] = min(dp[j], dp[j - i * i] + 1);
        return dp[n];
    }
};

57、474. 一和零
①状态表示:(二维费用)
dp[i][j][k]表示:从前i个完全平方数中挑选,字符0的个数不超过j,字符1的个数不超过k,所有的选法中,最大的长度。
②状态转移方程:根据最后一步的状况,分情况讨论。
不选str[i]→dp[i][j][k]=dp[i-1][j][k]
选str[i]→dp[i][j][k]=dp[i-1][j-a][k-b]+1 ?(j>=a&&k>=b)
dp[i][j]=max(dp[i-1][j][k],dp[i-1][j-a][k-b]+1)
③初始化:
④填表顺序:i从小到大。
⑤返回值:dp[len][m][n]。

class Solution 
{
public:
    int findMaxForm(vector<string>& strs, int m, int n) 
    {
        int len = strs.size();
        vector<vector<vector<int>>> dp(len + 1, vector<vector<int>>(m + 1, vector<int>(n + 1)));

        for(int i = 1; i <= len; i++)
        {
            int a = 0, b = 0;
            for(auto ch : strs[i - 1])
                if(ch == '0') a++;
                else b++;
            
            for(int j = 0; j <= m; j++)
                for(int k = 0; k <= n; k++)
                {
                    dp[i][j][k] = dp[i - 1][j][k];
                    if(j >= a && k >= b)
                        dp[i][j][k] = max(dp[i][j][k], dp[i - 1][j - a][k - b] + 1);
                }
        }
        return dp[len][m][n];
    }
};

优化:

class Solution 
{
public:
    int findMaxForm(vector<string>& strs, int m, int n) 
    {
        int len = strs.size();
        vector<vector<int>> dp(m + 1, vector<int>(n + 1));

        for(int i = 1; i <= len; i++)
        {
            int a = 0, b = 0;
            for(auto ch : strs[i - 1])
                if(ch == '0') a++;
                else b++;
            // 从大到小优化
            for(int j = m; j >= a; j--)
                for(int k = n; k >= b; k--)
                    dp[j][k] = max(dp[j][k], dp[j - a][k - b] + 1);
        }
        return dp[m][n];
    }
};

58、879. 盈利计划
①状态表示:(二维费用)
dp[i][j][k]表示:从前i个计划中挑选,总人数不超过j,总利润至少为k,所有的选法中,一共有多少种选法。
②状态转移方程:根据最后一步的状况,分情况讨论。
不选i→dp[i][j][k]=dp[i-1][j][k]
选i→dp[i][j][k]=dp[i-1][j-g[i]][max(0,k-p[i])]?j>=g[i]
dp[i][j][k]=dp[i-1][j][k]+dp[i-1][j-g[i]][max(0,k-p[i])]?
dp[i][j][k]%=1e9+7
③初始化:dp[0][j][0]=1
④填表顺序:i从小到大。
⑤返回值:dp[len][m][n]。

class Solution 
{
public:
    int profitableSchemes(int n, int m, vector<int>& g, vector<int>& p) 
    {
        const int MOD = 1e9 + 7;
        int len = g.size();
        vector<vector<vector<int>>> dp(len + 1, vector<vector<int>>(n + 1, vector<int>(m + 1)));
        for(int j = 0; j <= n; j++) dp[0][j][0] = 1;
        for(int i = 1; i <= len; i++)
            for(int j = 0; j <= n; j++)
                for(int k = 0; k <= m; k++)
                {
                    dp[i][j][k] = dp[i-1][j][k];
                    if(j >= g[i - 1]) dp[i][j][k] += dp[i - 1][j - g[i - 1]][max(0, k - p[i - 1])];
                    dp[i][j][k] %= MOD;
                }
        return dp[len][n][m];
    }
};

优化:

class Solution 
{
public:
    int profitableSchemes(int n, int m, vector<int>& g, vector<int>& p) 
    {
        const int MOD = 1e9 + 7;
        int len = g.size();
        vector<vector<int>> dp(n + 1, vector<int>(m + 1));
        for(int j = 0; j <= n; j++) dp[j][0] = 1;
        for(int i = 1; i <= len; i++)
            for(int j = n; j >= g[i - 1]; j--)
                for(int k = m; k >= 0; k--)
                {
                    dp[j][k] += dp[j - g[i - 1]][max(0, k - p[i - 1])];
                    dp[j][k] %= MOD;
                }
        return dp[n][m];
    }
};

59、377. 组合总和 Ⅳ
①状态表示:根据分析问题的过程中,发现重复子问题,抽象出来一个状态表示。
dp[i]表示:凑成总和i,一共有多少种排列数。
②状态转移方程:根据最后一步的状况,分情况讨论。
if(i>=nums[j]) dp[i]=dp[i-nums[j]]
dp[i]+=[i-nums[j]]
③初始化:dp[0]=1
④填表顺序:从左到右。
⑤返回值:dp[target]。

class Solution 
{
public:
    int combinationSum4(vector<int>& nums, int target) 
    {
        vector<double> dp(target + 1);
        dp[0] = 1;
        for(int i = 1; i <= target; i++)
            for(auto x : nums)
                if(i >= x)
                    dp[i] += dp[i - x];
        return dp[target];
    }
};

60、96. 不同的二叉搜索树(左子树<根<右子树)
①状态表示:根据分析问题的过程中,发现重复子问题,抽象出来一个状态表示。
dp[i]表示:结点个数为i时,一共有多少种二叉搜索树。
②状态转移方程:根据最后一步的状况,分情况讨论。
卡特兰数:dp[i]+=dp[j-1]*dp[i-j] (1<=j<=i)
③初始化:dp[0]=1
④填表顺序:从左到右。
⑤返回值:dp[n]。

class Solution 
{
public:
    int numTrees(int n) 
    {
        vector<int> dp(n + 1);
        dp[0] = 1;
        for(int i = 1; i <= n; i++)
            for(int j = 1; j <= i; j++)
                dp[i] += dp[i - j] * dp[j - 1];

        return dp[n];
    }
};

文章来源:https://blog.csdn.net/RTX__90/article/details/135476218
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。