《算法笔记》第十一章动态规划

算法笔记第十一章目录

    • 1.动态规划基本概念
    • 2.最长子序列和
      • 分析
      • 代码
      • 练习题
    • 3.最长不下降序列
      • 分析
      • 代码
      • 练习题
    • 4.最长公共序列LCS
      • 分析
      • 代码
      • 练习题
    • 5.最长回文序列
      • 分析
      • 代码
      • 练习题
    • 6.DAG最长路径
      • 分析
      • 代码
      • 练习题
    • 7.背包问题——01背包
      • 分析
      • 代码
    • 8.背包问题——完全背包
      • 分析
      • 代码
      • 练习题


1.动态规划基本概念

  1. 重叠子问题:如果一个问题能分成多个子问题,而且这多个子问题是会重复出现的,便是重叠子问题。如求解斐波那契额数列,求F(4)与F(5)都要调用F(3),F(3)这个子问题出现多次

  2. 最优子结构:如果一个问题的最优解是由其子问题的最优解组成的话,则这个问题便具有最优子结构

  3. 状态的无后效性:一旦当前状态被确定就不会再改变

动态规划便是解决这种拥有重叠子问题和最优子结构的问题的方法,动态规划的重点是找到状态转移方程
注:动态规划问题还需要注意边界的选取,边界是预先已经确定了结果的一系列子问题。对于动态规划可解的问题,并不是所有的状态都有无后效性,需要找到一系列无后效性的状态并建立相应的状态转移方程。

2.最长子序列和

例题:有{-2,11,-4,13,-5,-2}序列,求连续子序列加和的最大值。
注,连续子序列就是形如“-2,11”、“11,-4,13”的子序列。

分析

预先设序列数组为num数组,num[0]=-2,num[1]=11。设以第i个数字结尾的子序列的最大值数组为dp数组,dp[i]代表以第i个数字结尾的子序列和的最大值。

可以使用暴力枚举左右端点,也可以计算前缀和然后计算子序列和。使用动态规划,我们可以这样分解问题:所有的子序列可以划分为“以-2结尾的子序列”、“以11结尾的子序列”、“以-4结尾的子序列”等一共6种子序列,最优解就是这六大种中的一个,即问题的最优解由子问题最优解组成——最优子结构。同时在计算“以11结尾的子序列和的最大值”的时候需要考虑“以-2结尾的子序列和的最大值”,即重叠子问题。同时我们发现“以11结尾的子序列和的最大值”要么等于11自己要么等于“以-2结尾的子序列和的最大值”加上11,即dp[1]=max(dp[0]+num[1],num[1])。由此我们可以归纳出状态转移方程dp[n]=max(dp[n-1]+num[n],num[n])边界也显而易见,就是dp[0]=num[0]

代码

#include
#include
using namespace std;
int main(){
    int n;
    cin>>n;
    int num[n],dp[n],maxSum;
    for(int i=0;i<n;i++)    cin>>num[i];
    maxSum=dp[0]=num[0];//边界
    for(int i=1;i<n;i++){
        dp[i]=max(num[i],num[i]+dp[i-1]);//状态转移方程
        if(dp[i]>maxSum)    maxSum=dp[i];
    }
    cout<<maxSum;
    return 0;
}

练习题

习题链接,AC代码:

/*
    num数组存储读入的序列
    dp数组存储以第i个数字结尾的子序列和最大值
    start数组存储最大和序列的第一个元素
    end数组存储最大和序列的最后一个元素
    maxIndex存储最大序列和的下标
*/
#include
using namespace std;
int main(){
    int n;
    while(true){
        cin>>n;
        if(n==0)    break;
        int num[n],dp[n],start[n],end[n],maxIndex=0;
        for(int i=0;i<n;i++)    cin>>num[i];
        start[0]=end[0]=dp[0]=num[0];//边界
        for(int i=1;i<n;i++){
            if(num[i]>num[i]+dp[i-1]){
                dp[i]=num[i];
                start[i]=num[i];
            }
            else{
                dp[i]=num[i]+dp[i-1];
                start[i]=start[i-1];
            }
            end[i]=num[i];
            if(dp[i]>dp[maxIndex])    maxIndex=i;
        }
        if(dp[maxIndex]<0)  cout<<"0 "<<num[0]<<" "<<num[n-1]<<endl;
        else                cout<<dp[maxIndex]<<" "<<start[maxIndex]<<" "<<end[maxIndex]<<endl;
    }
    return 0;
}

3.最长不下降序列

例题:有{-2,11,-4,13,-5,-1}序列,求最长不下降序列的长度,序列元素可以不连续但必须保持相对顺序。
注,不下降序列就是形如“-2,11”、“11,13”的序列。

分析

预先设序列数组为num数组,num[0]=-2,num[1]=11。设以第i个数字结尾的不下降序列的最大长度数组为dp数组,dp[i]代表以第i个数字结尾的不下降序列的最大长度。

可以使用暴力枚举左右端点,也可以计算前缀和然后计算子序列和。使用动态规划,我们可以这样分解问题:所有的序列可以划分为“以-2结尾的子序列”、“以11结尾的子序列”、“以-4结尾的子序列”等一共6种子序列,最优解就是这六大种中的一个,即问题的最优解由子问题最优解组成——最优子结构。同时在计算“以11结尾的不下降序列的最大长度”的时候需要考虑“以-2结尾的不下降序列的最大长度”,即重叠子问题。

同时我们发现“以-1结尾的不下降序列的最大长度”等于所有在-1前面且不比-1大的元素对应的dp值加一的最大值,即位于第5个元素-1前面且小于等于-1有第0个元素-2、第2个元素-4、第4个元素-5,则dp[5]=max{dp[0]+1,dp[2]+1,dp[4]+1},即下面这段代码:

for(int j=0;j<i;j++)
	if(num[i]>=num[j])//所有在第i个元素前面的元素且不比第i个元素大的元素
		dp[i]=max(dp[i],dp[j]+1);//找到在第i个元素前面且不比第i个元素大的元素的dp值加1的最大值

上述代码就是状态转移方程边界也显而易见,就是dp[0]=1

代码

#include
#include
using namespace std;
int main(){
    int n;
    cin>>n;
    int num[n],dp[n],maxLen=0;
    for(int i=0;i<n;i++)    cin>>num[i];
    for(int i=0;i<n;i++){
        dp[i]=1;//边界
        //状态转移方程
        for(int j=0;j<i;j++)
            if(num[i]>=num[j])  dp[i]=max(dp[i],dp[j]+1);
        if(dp[i]>maxLen)    maxLen=dp[i];
    }
    cout<<maxLen;
    return 0;
}

练习题

习题链接,AC代码(与上面的例题代码相同):

#include
#include
using namespace std;
int main(){
    int n;
    cin>>n;
    int num[n],dp[n],maxLen=0;
    for(int i=0;i<n;i++)    cin>>num[i];
    for(int i=0;i<n;i++){
        dp[i]=1;//边界
        //状态转移方程
        for(int j=0;j<i;j++)
            if(num[i]>=num[j])  dp[i]=max(dp[i],dp[j]+1);
        if(dp[i]>maxLen)    maxLen=dp[i];
    }
    cout<<maxLen;
    return 0;
}

4.最长公共序列LCS

给定两字符串sadstory、adminsorry,求一个字符串,使这个字符串是前面两字符串的最长公共部分
注,子序列可以不连续。

分析

设置二维数组dp,dp[i][j]表示前一个字符串i号位和字符串j号位(字符串从1开始编号)之前的最长公共序列长度。
我们分析dp[i][j]
说明:因为我读取字符串的时候是直接读取,所以字符串下标从0开始,但dp中字符串从1开始,所以下面取字符串单个字符时,相应的下标要减一。
A[i-1]=B[j-1]时,说明当前位匹配,则在原先最长公共序列长度基础上加一,即dp[i][j]=dp[i-1][j-1]+1
A[i-1]!=B[j-1]时,说明当前位不匹配,则说明现最长公共序列等于dp[i-1][j]dp[i][j-1]中的最大值(在计算dp[i][j]之前,dp[i-1][j]dp[i][j-1]已经计算完成),即dp[i][j]=max(dp[i-1][j],dp[i][j-1])。可以得到如下状态转移方程:

if(a[i-1]==b[j-1])	dp[i][j]=dp[i-1][j-1]+1;
else            	dp[i][j]=max(dp[i-1][j],dp[i][j-1]);

上述代码就是状态转移方程边界也显而易见,就是dp数组任意维度为0的值都为0。

代码

#include
#include
using namespace std;
int main(){
    string a,b;
    cin>>a>>b;
    int dp[a.length()+1][b.length()+1];
    for(int i=0;i<=a.length();i++)   dp[i][0]=0;//边界
    for(int i=0;i<=b.length();i++)   dp[0][i]=0;//边界
    for(int i=1;i<=a.length();i++)
        for(int j=1;j<=b.length();j++)
            //状态转移方程
            if(a[i-1]==b[j-1])  dp[i][j]=dp[i-1][j-1]+1;
            else            dp[i][j]=max(dp[i-1][j],dp[i][j-1]);
    cout<<dp[a.length()][b.length()];
    return 0;
}
/*
字符串A:sadstory
字符串B:adminsorry
dp数组如下:
0 0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 1 1 1 1 1
0 1 1 1 1 1 1 1 1 1 1
0 1 2 2 2 2 2 2 2 2 2
0 1 2 2 2 2 3 3 3 3 3
0 1 2 2 2 2 3 3 3 3 3
0 1 2 2 2 2 3 4 4 4 4
0 1 2 2 2 2 3 4 5 5 5
0 1 2 2 2 2 3 4 5 5 6
*/

练习题

习题链接,AC代码(与上面的例题代码相同):

#include
#include
using namespace std;
int main(){
    string a,b;
    while(cin>>a>>b){
        int dp[a.length()+1][b.length()+1];
        for(int i=0;i<=a.length();i++)   dp[i][0]=0;
        for(int i=0;i<=b.length();i++)   dp[0][i]=0;
        for(int i=1;i<=a.length();i++)
        for(int j=1;j<=b.length();j++)
            if(a[i-1]==b[j-1])  dp[i][j]=dp[i-1][j-1]+1;
            else            dp[i][j]=max(dp[i-1][j],dp[i][j-1]);
        cout<<dp[a.length()][b.length()]<<endl;
    }
    return 0;
}

5.最长回文序列

给定字符串aabbaa31dadeymasdas121ccbbcc
求最长回文子串的长度与最长回文子串
注:如果有多个最长回文子串,则优先打印左端点最靠左的子串

分析

设原始字符串为s,设置二维数组dp,dp[i][j]表示起始字符为第i个字符,结尾字符为第j个字符的字符串(字符下标从0开始)是否是回文串,0为否,1为是。
我们分析dp[i][j]
s[i]==s[j],说明从i到j的字符串有可能是回文串,此时需要判断除了首尾字符的子串是否是回文串,所以此时dp[i][j]=dp[i+1][j-1],从i+1到j-1是回文串的话则从i到j的字符串也是回文串,否则就不是。
s[i]!=s[j],说明从i到j的字符串一定不是回文串,因为首尾字符就已经不相同了。
综上所述,可以得到如下状态转移方程:

if(s[i]==s[j])  dp[i][j]=dp[i+1][j-1];
else            dp[i][j]=0;

上述代码就是状态转移方程

我们分析边界
1.一个字符也是回文串,所以dp[i][i]=1
2.因为dp[i][j]代表从i到j的字符串,若i>j,则该字符串不存在,所以当i>j,dp[i][j]=0
3.因为上述状态转移方程dp[i][j]dp[i+1][j-1]有关,看一个例子,当计算dp[0][1]dp[3][4]时,我们发现需要计算dp[1][0]dp[4][3]。此时dp[1][0]dp[4][3]这种情况并不存在,但dp[0][1]dp[3][4]都可能为1,如“aa”。可以看出来当序号相邻时即我们需要让i+1<=j-1推得j>=i+2,所以在序号相邻的情况下,在状态转移中是无法计算的,所以需要做为边界。上述思路即if(s[i]==s[i+1]) dp[i][i+1]=1

此时,需要注意一个地方就是状态转移方程中的dp[i][j]dp[i+1][j-1],在一般的按行优先遍历计算中,无法保证dp[i+1][j-1]在计算dp[i][j]之前已经计算完毕。但我们发现dp[i+1][j-1]位于dp[i][j]的右下角,即按列计算可以满足dp[i+1][j-1]在计算dp[i][j]之前已经计算完毕。故下面代码均采用按列优先遍历计算。
在遍历计算中需要保证右端点大于等于左端点,并且dp值为1的元素值不再改变,所以在状态转移之前需要判断dp值是否为0,为0才可以进行改变。

代码

#include
#include
using namespace std;
int main(){
    string s;
    cin>>s;
    int dp[s.length()][s.length()],left,right;
    memset(dp,0,sizeof(dp));				//边界2
    //初始化边界
    for(int i=0;i<s.length();i++){
        dp[i][i]=1;							//边界1
        if(s[i]==s[i+1])    dp[i][i+1]=1;	//边界3
    }
    //状态转移方程
    for(int i=0;i<s.length();i++)		//列序号,按列优先
        for(int j=0;j<s.length();j++)	//行序号
            if(i>j&&dp[j][i]!=1){						//注意此时j是行号,i是列号
                if(s[i]==s[j])  dp[j][i]=dp[j+1][i-1];	//注意此时j是行号,i是列号
                else            dp[j][i]=0;				//注意此时j是行号,i是列号
                //记录最长回文序列的左右端点下标
                if(dp[j][i]==1&&i-j+1>right-left+1){
                    left=j;
                    right=i;
                }
            }
    cout<<"最大回文序列长度为"<<right-left+1<<",对应回文串为"<<s.substr(left,right-left+1);
    return 0;
}
/*
输入:aabbaa31dadeymasdas121ccbbcc
输出:最大回文序列长度为6,对应回文串为aabbaa
*/

练习题

思路:先把原始字符串pre处理为没有符号没有空格的字符串processed,处理过程中记录processed的字符对应pre中的下标,便于最后从processed的字符下标变换到pre的字符下标。回文串的运算同上述过程。
习题链接,AC代码:

#include
#include
#include
using namespace std;
int main(){
    string s;
    getline(cin,s);//不能使用cin,因为cin读取字符串遇到空白字符就会停止,而getline能读取一整行
    string processed_str="",str=s;
    int arr[str.length()];
    for(int i=0;i<str.length();i++)
        if(isalnum(str[i])){
            processed_str+=str[i];
            arr[processed_str.length()-1]=i;
        }
    transform(processed_str.begin(),processed_str.end(),processed_str.begin(),::tolower);
    int dp[processed_str.length()][processed_str.length()],left=0,right=0;
    memset(dp,0,sizeof(dp));
    for(int i=0;i<processed_str.length();i++){
        dp[i][i]=1;
        if(processed_str[i]==processed_str[i+1])    dp[i][i+1]=1;
    }
    for(int i=0;i<processed_str.length();i++)//列序号
        for(int j=0;j<processed_str.length();j++)//行序号
            if(i>j&&dp[j][i]!=1){
                if(processed_str[i]==processed_str[j])  dp[j][i]=dp[j+1][i-1];
                else            dp[j][i]=0;
                if(dp[j][i]==1&&i-j+1>right-left+1){
                    left=j;
                    right=i;
                }
            }
    cout<<s.substr(arr[left],arr[right]-arr[left]+1);
    return 0;
}

6.DAG最长路径

给定一个有向无环图,求图中的最长路径

分析

dp[i]表示从第i个点出发的路径的最大长度。根据逆向拓扑排序的顺序计算,若第i个点拓扑顺序后面的所有结点dp值已经确定,则dp[i]=max{dp[j](j是i拓扑排序后面的点,且i到j有边)}+length(i,j),为了避免求解拓扑序列,可以使用递归的方法求解。
求解dp[i],也就是求解所有从i可以到达的点的最大值再加上i到j的边长。可以按照结点序号的顺序进行求解,递归的过程中会修改路径中的结点的dp值,这种方法的缺点就是对于dp值本来就是0的结点会浪费一些时间在for循环内。

for(int j=0;j<n;j++)
	if(G[i][j]!=INF&&G[i][j]!=0){
		int temp=DP(j)+G[i][j];
		if(temp>dp[i])	dp[i]=temp;
    }

代码

只求最长路径长度版

#include
#include
#include
#define INF 999
using namespace std;
int G[INF][INF],dp[INF]={0},n,maxLen=0;
void init(){
    memset(G,INF,sizeof(G));
}
int DP(int i){
    if(dp[i]>0) return dp[i];
    for(int j=0;j<n;j++)
        if(G[i][j]!=INF&&G[i][j]!=0){
            int temp=DP(j)+G[i][j];
            if(temp>dp[i])	dp[i]=temp;
        }
    return dp[i];
}
int main(){
    cin>>n;
    for(int i=0;i<n;i++)
        for(int j=0;j<n;j++)	cin>>G[i][j];
    for(int i=0;i<n;i++){
        dp[i]=DP(i);
        if(dp[i]>maxLen)	maxLen=dp[i];
    }
    cout<<<<maxLen<<endl;
    return 0;
}
/*
输入:
5
0 1 1 999 999
999 0 999 2 999
999 999 0 2 999
999 999 999 0 3
999 999 999 999 0
输出:
6
*/

求最长路径经过的点与长度版

#include
#include
#include
#include
#define INF 999
using namespace std;
int G[INF][INF],dp[INF]={0},n,maxLen=0,maxIndex=0;
vector<int> after[INF];
void init(){
    memset(G,INF,sizeof(G));
}
int DP(int i){
    if(dp[i]>0) return dp[i];
    for(int j=0;j<n;j++)
        if(G[i][j]!=INF&&G[i][j]!=0){
            int temp=DP(j)+G[i][j];
            if(temp>dp[i]){
                dp[i]=temp;
                after[i].clear();
                after[i].push_back(j);
            }
            else if(temp==dp[i]){
                after[i].push_back(j);
            }
        }
    return dp[i];
}
int main(){
    cin>>n;
    for(int i=0;i<n;i++)
        for(int j=0;j<n;j++)
            cin>>G[i][j];
    for(int i=0;i<n;i++){
        dp[i]=DP(i);
        if(dp[i]>maxLen){
            maxLen=dp[i];
            maxIndex=i;
        }
    }
    cout<<maxIndex<<","<<maxLen<<endl;
    return 0;
}
/*
输入:
5
0 1 1 999 999
999 0 999 2 999
999 999 0 2 999
999 999 999 0 3
999 999 999 999 0
输出:
0,6
*/

练习题

思路:将矩形看作一个结点,结点之间的边代表矩形是否能嵌套,小矩形代表的结点指向大矩形代表的结点。求最大序列长度也就转化为了求DAG中的路径最大长度
习题链接,AC代码:

#include
#include
#include
using namespace std;
int G[999][999],dp[999]={0},n,num,maxLen=0;
void init(){
    memset(G,0,sizeof(G));
    memset(dp,0,sizeof(dp));
    maxLen=0;
}
int DP(int i){
    if(dp[i]>0) return dp[i];
    for(int j=0;j<num;j++)
        if(G[i][j]!=0){
            int temp=DP(j)+G[i][j];
            if(temp>dp[i])  dp[i]=temp;
        }
    return dp[i];
}
bool judge(int first1,int second1,int first2,int second2){
    if((first2>first1&&second2>second1)||(first2>second1&&second2>first1))
        return true;
    return false;
}
int main(){
    cin>>n;
    while(n--){
        cin>>num;
        int rec[num][2];
        init();					//不要忘记每一轮都要进行相关变量的初始化
        for(int i=0;i<num;i++)
            cin>>rec[i][0]>>rec[i][1];
        for(int i=0;i<num;i++)
            for(int j=0;j<num;j++)
                if(judge(rec[j][0],rec[j][1],rec[i][0],rec[i][1]))
                    G[j][i]=1;
        for(int i=0;i<num;i++){
            dp[i]=DP(i);
            if(dp[i]>maxLen)    maxLen=dp[i];
        }
        cout<<maxLen+1<<endl;
    }
    return 0;
}

7.背包问题——01背包

n中物品,每种物品只有一件,背包总重量为V,求能放进背包物品的总价值的最大值

分析

物品重量数组weights,物品价值数组values,将物品从下标1开始编号
使用dp[x][y] 表示前 x 件物品,在不超过重量 y 的时候的最大价值。则有前x件物品重量不超过y的最大价值要么等于前x-1件物品重量不超过y的最大价值dp[x-1][y],要么等于前x-1件物品重量不超过y-weights[x]的最大价值加第x件物品的价值dp[x-1][y - weights[x]] + values[x]
所以可得:

dp[x][y] = Math.Max{dp[x-1][y], dp[x-1][y-weights[x]]+values[x]}

边界也显而易见,当没有物品或背包容量为0时,dp值都为0,所以可以将矩阵dp全部元素初始化为0

代码

#include
#include
#include
using namespace std;
int main(){
    int n,v;
    cin>>v>>n;
    int weights[n+1],values[n+1],dp[n+1][v+1];
    memset(dp,0,sizeof(dp));
    for(int i=1;i<=n;i++)    cin>>weights[i]>>values[i];
    for(int i=1;i<=n;i++)
        for(int j=1;j<=v;j++){
            dp[i][j]=dp[i-1][j];
            if(j>=weights[i])
                dp[i][j]=max(dp[i][j],dp[i-1][j-weights[i]]+values[i]);
        }
    cout<<dp[n][v]<<endl;
    return 0;
}

8.背包问题——完全背包

n中物品,每种物品有无数件,背包总重量为V,求能放进背包物品的总价值的最大值

分析

物品重量数组weights,物品价值数组values,将物品从下标1开始编号
使用dp[x][y] 表示前 x 件物品,在不超过重量 y 的时候的最大价值。则有前x件物品重量不超过y的最大价值要么等于前x-1件物品重量不超过y的最大价值dp[x-1][y],要么等于前x件物品重量不超过y-weights[x]的最大价值加第x件物品的价值dp[x-1][y - weights[x]] + values[x],因为物品有无数件,所以状态转移方程与01背包有不同。
所以可得:

dp[x][y] = Max(dp[x-1][y], dp[x][y-weights[x]]+values[x])
//注意01背包为Max(dp[x-1][y], dp[x-1][y-weights[x]]+values[x])

边界也显而易见,当没有物品或背包容量为0时,dp值都为0,所以可以将矩阵dp全部元素初始化为0

代码

练习题

  1. 装箱问题
    思路:dp[i][j]代表前i种物品在背包容量为j时的最小空闲容量,根据递推关系,dp[i][j]要么等于前i-1种物品在背包容量为j时的最小空闲容量,要么等于前i-1种物品在背包容量为j-volumes[i]的最小空闲容量再减去volumes[i]。即状态转移方程
dp[i][j]=dp[i-1][j];
if(j>=volumes[i])
	dp[i][j]=min(dp[i][j],dp[i-1][j-volumes[i]]-volumes[i]);

AC代码:

#include
#include
using namespace std;
int main(){
    int n,v;
    cin>>v>>n;
    int volumes[n+1],dp[n+1][v+1];
    for(int i=0;i<=n;i++)   dp[i][0]=v;//边界
    for(int i=0;i<=v;i++)   dp[0][i]=v;//边界
    for(int i=1;i<=n;i++)   cin>>volumes[i];
    for(int i=1;i<=n;i++)
        for(int j=1;j<=v;j++){
            dp[i][j]=dp[i-1][j];
            if(j>=volumes[i])
                dp[i][j]=min(dp[i][j],dp[i-1][j-volumes[i]]-volumes[i]);
        }
    cout<<dp[n][v]<<endl;
    return 0;
}
  1. 采药
    思路:01背包的思路
#include
#include
#include
using namespace std;
int main(){
    int totalTime,totalNum;
    cin>>totalTime>>totalNum;
    int dp[totalTime+1][totalNum+1],value[totalNum],time[totalNum];
    memset(dp,0,sizeof(dp));
    for(int i=0;i<totalNum;i++) cin>>time[i]>>value[i];
    for(int i=1;i<=totalTime;i++)
        for(int j=1;j<=totalNum;j++){
            dp[i][j]=dp[i][j-1];
            if(i>=time[j-1])
                dp[i][j]=max(dp[i-time[j-1]][j-1]+value[j-1],dp[i][j-1]);
        }
    cout<<dp[totalTime][totalNum];
    return 0;
}
  1. 货币系统
    二维矩阵解法,这个解法OJ只得50分。自己试了几个样例,发现跟滚动数组解法答案一样,暂时找不到错在哪里,推测是边界跟迭代边界的问题。
#include
#include
using namespace std;
int main(){
    int n,v;
    cin>>n>>v;
    long long values[n+1],dp[n+1][v+1];
    memset(dp,0,sizeof(dp));
    for(int i=1;i<=n;i++){
        cin>>values[i];
        dp[i][0]=1;
    }
    dp[1][0]=1;
    for(int i=1;i<=n;i++)
        for(int j=1;j<=v;j++){
            dp[i][j]=dp[i-1][j];
            if(j>=values[i])
                dp[i][j]+=dp[i][j-values[i]];
        }
    cout<<dp[n][v]<<endl;
    return 0;
}

滚动数组解法,这个解法是AC代码

#include
#include 
#include
using namespace std;
int w[28];
long long dp[10008];
int main(){
	int m,n,i,v;
	while(cin>>n>>m){
		memset(dp,0,sizeof(dp));
		dp[0]=1;
		for(i=0;i<n;i++)	cin>>w[i];
		for(i=0;i<n;i++){
			for(v=w[i];v<=m;v++)
				dp[v]=dp[v]+dp[v-w[i]];
		cout<<dp[m]<<endl;
	}
}

你可能感兴趣的:(算法笔记,C++)