DP =「状态」+「阶段」+「决策」
基本原理 = 「有向无环图」+「最优子结构」+「无后效性」
目录
【例题4】poj 2279 Picture Permutations
【例题7】poj 3666 Making the Grade
零. 相关概念辨析
(1)递归和递推
1. 递归从最终目标出发,将复杂问题化为简单问题,逆向解答。
2. 递推从简单问题出发,一步步的向前发展,正向解答。
(2)记忆化搜索
先将数组初始化,再在递归的过程中判断此处是否已经计算过。
(3)刷表法和填表法
填表法:利用方程和上一状态推导现在的状态(已知条件,填答案)。
刷表法:利用当前的状态,把有关联的下一状态都推出来。
一. 线性DP
{ 1.概念引入 }
解决一些线性区间上的最优化问题,会用到DP的思想,这种问题可以叫做线性DP。
DP的阶段沿各个维度线性增长,从边界点开始,有方向地向状态空间转移、扩展。
若“决策集合中的元素只增多不减少”,可以维护一个变量记录决策集合信息,避免重复扫描。
需要考虑:一个后续阶段的未知状态,需要哪些前阶段的已知状态去更新。
经典模型:最长上升子序列(LIS)、最长公共子序列(LCS)、最大子序列和、数字三角形等。
那么首先我们从这几个经典的问题出发开始对线性DP的探索。
{ 2.例题详解 }
【例题1】caioj 1064 最长上升子序列
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
typedef unsigned long long ull;
/*【caioj 1064】最长上升子序列
n个不相同的整数组成的数列a(1)、a(2)、……、a(n)。
求出其中最长的上升序列(LIS)的长度。 */
//【线性DP】【匹配转移】
//状态:f[i]表示以a[i]为结尾的、最长上升子序列的长度。
//方程:f[i]=max{ f[j]+1 (j<i,a[j]<a[i]) };
//边界:f[0]=0; f[i]=1; 目标:max{f[i]};
int a[1009],f[1009];
int main(){
int n,ans=0; scanf("%d",&n);
for(int i=1;i<=n;i++) scanf("%d",&a[i]);
for(int i=1;i<=n;i++) f[i]=1;
for(int i=2;i<=n;i++)
for(int j=i-1;j>=f[i];j--)
//↑↑↑【剪枝】j>=f[i]:如果j小于目前长度,不可能使答案更新
if(a[j]<a[i]) f[i]=max(f[i],f[j]+1);
for(int i=1;i<=n;i++) if(ans<f[i]) ans=f[i];
printf("%d\n",ans); return 0;
}
【例题2】caioj 1068 最长公共子序列
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
typedef unsigned long long ull;
/*【caioj 1068】最长公共子序列
给出两个字符串 S1 和 S2 ,求它们最长公共子序列的长度。 */
//【线性DP】【匹配转移】
//状态:f[i][j]表示前缀子串s[1~i]与t[1~j]的最长公共子序列长度。
//方程:f[i][j]=max{ f[i-1][j], f[i][j-1], f[i-1][j-1]+1(if s[i]=t[j]) };
//边界:f[i][0]=f[0][j]=0; 目标:f[n][m];
const int maxn=1009;
char s[maxn],t[maxn]; //两个字符串
int f[maxn][maxn]; //前缀子串s[1~i]与t[1~j]的最长公共子序列长度
int main(){
scanf("%s%s",s,t);
int lens=strlen(s),lent=strlen(t);
memset(f,0,sizeof(f));
for(int i=1;i<=lens;i++)
for(int j=1;j<=lent;j++){
f[i][j]=max(f[i-1][j],f[i][j-1]);
if(s[i-1]==t[j-1]) //如果元素公共
f[i][j]=max(f[i][j],f[i-1][j-1]+1); //匹配字符成功
}
printf("%d\n",f[lens][lent]);
return 0;
}
【例题3】洛谷 p1216 数字三角形
- [ 数字三角形 ] 问题的特点:具有大量重叠子问题。
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
typedef unsigned long long ull;
/*【洛谷p1216】数字三角形
一个三角形,每一步可以走到左下方的点也可以到达右下方的点。
查找从最高点到底部任意处结束的路径,使路径经过数字的和最大。 */
//【线性DP】【路径转移】
//状态:f[i][j]表示从左上角走到第i行第j列,得到的最大的和。
//方程:f[i][j]=a[i][j]+max{f[i-1][j],f[i-1][j-1](if j>1)};
//边界:f[1][1]=a[1][1];
//目标:max{f[N][j]};
int f[1009][1009],a[1009][1009];
int main(){
int n,maxx=0; scanf("%d",&n);
memset(f,0,sizeof(f));
for(int i=1;i<=n;i++)
for(int j=1;j<=i;j++){
scanf("%d",&a[i][j]);
f[i][j]=a[i][j];
}
for(int i=1;i<=n;i++)
for(int j=1;j<=n;j++){
if(j>1) f[i][j]+=max(f[i-1][j],f[i-1][j-1]);
else f[i][j]+=f[i-1][j];
}
for(int j=1;j<=n;j++)
maxx=max(maxx,f[n][j]);
printf("%d\n",maxx);
return 0;
}
【例题4】poj 2279 Picture Permutations
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
typedef unsigned long long ull;
/*【poj2279】合影
有n个学生,站成k排,给出每排的人数,
要求每排身高从左往右递增,每列身高从前往后递增。
问有多少种排列方式。 */
//【线性DP】【匹配转移】【方案数DP】
//状态:f[a1,a2,a3,a4,a5]表示每排站了a1,a2,a3,a4,a5个人的方案数。
//方程:当a1<N1,f[a1+1,a2,a3,a4,a5]+=f[a1,a2,a3,a4,a5];
// 当a2<N2且a1>a2时,f[a1,a2+1,a3,a4,a5]+=f[a1,a2,a3,a4,a5];
//边界:f[0,0,0,0,0]=1; 目标:f[N1,N2,N3,N4,N5];
long long f[31][31][31][31][31]; //会MLE
int lim[6]; //每排的人数
int main(){
int n,k; while(cin>>n&&n) {
memset(f,0,sizeof(f));
memset(lim,0,sizeof(lim));
for(int i=1;i<=n;i++) cin>>lim[i];
f[0][0][0][0][0]=1;
for(int i=0;i<=lim[1];i++)
for(int j=0;j<=lim[2];j++)
for(int k=0;k<=lim[3];k++)
for(int a=0;a<=lim[4];a++)
for(int b=0;b<=lim[5];b++){
if(i+1<=lim[1])
f[i + 1][j][k][a][b]+=f[i][j][k][a][b];
if(j+1<=lim[2]&&j<i)
f[i][j + 1][k][a][b]+=f[i][j][k][a][b];
if(k+1<=lim[3]&&k<j&&k<i)
f[i][j][k + 1][a][b]+=f[i][j][k][a][b];
if(a+1<=lim[4]&&a<i&&a<j&&a<k)
f[i][j][k][a + 1][b]+=f[i][j][k][a][b];
if(b+1<=lim[5]&&b<i&&b<j&&b<k&&b<a)
f[i][j][k][a][b + 1]+=f[i][j][k][a][b];
}
cout<<f[lim[1]][lim[2]][lim[3]][lim[4]][lim[5]]<<endl;
}
return 0;
}
【例题5】最长公共上升子序列(LCIS)
- 给出有 n 个元素的数组 a[ ],m 个元素的数组 b[ ] 。
- 求出它们的最长上升公共子序列的长度。
a[] data:
5
1 4 2 5 -12
b[] data:
4
-12 1 2 4
LCIS is 2
LCIS 所含元素为 1 4
1.确定状态
dp[i][j] 表示以 a 数组的前 i 个整数与 b 数组的前 j 个整数且以 b[j] 为结尾构成的公共子序列长度。
需要注意,以 b[j] 结尾构成的公共子序列的长度不一定是最长公共子序列的长度!
2.确定状态转移方程
(1)当 a[i] == b[j] 时,我们只需要在前面找到一个能将 b[j] 接到后面的最长的公共子序列。
之前最长的公共子序列在哪呢?首先我们要去找的 dp[ ][ ] 的第一维必然是 i - 1 。
第二维呢?那就需要枚举 b[1]...b[j-1] 了,因为你不知道这里面哪个最长且哪个小于 b[j] 。
可不可能不配对呢?也就是在 a[i] == b[j] 的情况下,需不需要考虑 dp[i][j] == dp[i-1][j] 的决策呢?
答案是不需要。如果 b[j] 不和 a[i] 配对,就是和之前的 a[1]...a[j-1] 配对,必然没有和 a[i] 配对优越。
(b[j] 和 a[i] 配对之后转移为 max(dp[i-1][k])+1,和前面的 i1 配对则是 max(dp[i1-1][k])+1 。)
(2)当 a[i] != b[j] 时, 因为 dp[i][j] 是以 b[j] 为结尾的LCIS,
如果 dp[i][j] > 0,那么就说明 a[1]..a[i] 中必然有一个整数 a[k] 等于 b[j] 。
因为 a[k] != a[i] ,那么 a[i] 对 dp[i][j] 没有贡献,于是我们不考虑它照样能得出 dp[i][j] 的最优值。
所以在 a[i] != b[j] 的情况下必然有 dp[i][j] == dp[i-1][j] 。
- 综上所述,我们可以得到状态转移方程:
① a[i] != b[j], dp[i][j] = dp[i-1][j]。
② a[i] == b[j], dp[i][j] = max(dp[i-1][k]+1) (1 <= k <= j-1 && b[j] > b[k])。
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
typedef unsigned long long ull;
/* 最长公共上升子序列(LCIS)
给出有 n 个元素的数组 a[ ],m 个元素的数组 b[ ] 。
求出它们的最长上升公共子序列的长度。 */
//【线性DP】【匹配转移】
//状态:dp[i][j]表示【a的前i个】与【b的前j个】且【以b[j]为结尾】构成的LCIS长度。
//方程:① a[i]!=b[j],dp[i][j]=dp[i-1][j]。
// ② a[i]==b[j],dp[i][j]=max(dp[i-1][k]+1) (1<=k<=j-1 && b[j]>b[k])。
//边界:dp[0][i]=0,dp[i][0]=0;
//目标:中间过程中保存更新的答案ans。
//复杂度:O(N * M^2)
int a[3005],b[3005],dp[505][505]={0};
int lengthOfLCIS(int *a,int n,int *b,int m) {
int ans=0,max_dp=0; //max_dp记录上一个LCIS长度最大值
for(int i=0;i<n;++i)
for(int j=0;j<m;++j){
dp[i+1][j+1]=dp[i][j + 1];
if(a[i]==b[j]){
max_dp=0; //每次匹配时记得清零
for(int k=0;k<j;++k) //满足上升关系
if(b[j]>b[k] && max_dp<=dp[i][k+1])
max_dp=dp[i][k+1]; //记录上一个LCIS长度最大值
dp[i+1][j+1]=max_dp+1; //此处也能匹配
}
ans=ans>dp[i+1][j+1]?ans:dp[i + 1][j + 1]; //更新ans
}
return ans; //复杂度:O(N * M^2)
}
int main(){
int n,m,T; cin>>T;
while(T--){
scanf("%d",&n);
for(int i=0;i<n;i++) scanf("%d",&a[i]);
scanf("%d",&m);
for(int i=0;i<m;i++) scanf("%d",&b[i]);
printf("%d\n",lengthOfLCIS(a,n,b,m));
if(T) printf("\n"); //注意输出格式
}
return 0;
}
3.可以发现,这是O(N * M^2) 算法。
【优化1】O(N * M * log(M))算法
分析: 当a[i]==b[j]
时在前面找到一个能将 b[j] 接到后面的最大长度的过程,是可以优化的。
设置辅助数组 len[ i ] = value,代表【长度为 i 的所有LCIS】中、【最后一位数字的最小值】为value。
Q: 为什么要这样设计辅助数组 len[] ?
需要优化的过程是一个查找过程( dp[i][j] = max(dp[i-1][k]+1) (1 <= k <= j-1 && b[j] > b[k]) )
,
实际上,这个查找过程是在一个一维数组中查找满足条件 b[j] > b[k]
的最大值。
可以采用hash或者是二分将查找问题降到一个可以接受的复杂度,大体判断二分似乎更适合。
由于二分查找的对象必须具有单调性,而在最长公共上升子序列中,
所以的子序列是具有单调性的,所以我们可以将子序列的结尾数字作为数组中的值。
维护一个最小值,这样才能够优化判断 b[j] 到底能接在哪个位置。
Q: len[] 更新策略?
用二分查找找到 b[j] 能“嫁接”到之前子序列中的最长长度。
( ind是二分出来的len[]数组下标,根据上面的定义,ind就是以b[j]结尾的最长公共上升子序列长度 )。
当a[i] == b[j]
,这时候 b[j] 将“嫁接”到之前子序列中的合适位置,同时更新 len[ind] = b[j] 。
当a[i] != b[j]
,我们也需要更新 len[] 的信息,因为后面的更新需要前面的数据。
当 b[j] 与 a[i] 适配,需要查看 b[j] 与前 i - 1个数字所构成的以 b[j] 为结尾数字的最长的公共子序列。
如果有多个跟 dp[ i ][ j + 1 ]相同长度的子序列,如果比 d[j] 小则更新 len[dp[i][j + 1]]的值。
// 复杂度 O(N * M * log(M))
int binary_search(int *len, int length, int value) {
int l = 0, r = length - 1, mid;
while (l < r) {
mid = (l + r) >> 1;
if (len[mid] >= value) r = mid;
else l = mid + 1;
}
return l; //满足b[j]>len[LCIS长度]的最大LCIS长度
}
int lengthOfLCIS(int *a, int n, int *b, int m) {
int ans = 0;
int dp[505][505] = {0};
int len[505];
for (int i = 0 ; i < n ; ++i) {
len[0] = INT_MIN;
for (int j = 0 ; j < m ; ++j) {
len[j + 1] = INT_MAX;
int ind = binary_search(len, j + 2, b[j]);
if (a[i] != b[j]) {
if (b[j] < len[dp[i][j + 1]])
// 只有当b[j]的值小于当前len[dp[i][j + 1]]的最小值时才能更新
len[dp[i][j + 1]] = b[j];
dp[i + 1][j + 1] = dp[i][j + 1];
} else {
dp[i + 1][j + 1] = ind;
len[ind] = b[j];
}
ans = ans > dp[i + 1][j + 1] ? ans : dp[i + 1][j + 1];
}
}
return ans;
}
其他优化请戳 这里 ...
【例题6】三个字符串的最长公共子序列
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
typedef unsigned long long ull;
/*【caioj 1073】三个字符串的最长公共子序列
给出三个字符串,求它们最长公共子序列。 */
//【线性DP】【匹配转移】【多维DP】
//状态:a[i][j][k]表示第一个字符串的前i个字符,第二个字符串的前j个字符和
// 第三个字符串的前k个字符的最长公共子序列的长度。
const int MAXN = 101;
int a[MAXN][MAXN][MAXN];
//↓↓↓函数返回三个字符串的最长公共子序列的长度
int commonLen(string strA, string strB, string strC){
int n1 = strA.size();
int n2 = strB.size();
int n3 = strC.size();
for(int i=0;i<=n1;i++) //初始化:某串长度为0的情况
for(int j=0;j<=n2;j++)
for(int k=0;k<=n3;k++)
if(i==0||j==0||k==0) a[i][j][k]=0;
for(int i=1;i<=n1;i++)
for(int j=1;j<=n2;j++)
for(int k=1;k<=n3;k++){
if(strA[i-1]==strB[j-1]&&strB[j-1]==strC[k-1])
a[i][j][k]=a[i-1][j-1][k-1]+1;
else a[i][j][k]=max(max(a[i-1][j][k],a[i][j-1][k]),a[i][j][k -1]);
}
return a[n1][n2][n3];
}
int main(){
string str1, str2, str3;
while(cin >> str1 >> str2 >> str3)
cout << commonLen(str1, str2, str3) << endl;
return 0;
}
【例题7】poj 3666 Making the Grade
- 给出长度为n的整数数列,每次可以将一个数加1或者减1,
- 最少要多少次可以将其变成不严格的单调递增或者单调递减。
【分析】首先要证明ans最小化的情况下,一定存在一种构造方式,
使得b数组在a数组中全部出现过。证明方法简述:
记原来的数从小到大排序后分别是a1 a2 a3...an,修改后分别是b1 b2 b3...bn。
为了方便描述,在数轴上标出这些点,称为关键点。
- 假设存在as<bi<=bi+1<=...<=bj<as+1。
- 情况一:如果这些b都相等,那么把这些b都改成as或者as+1,肯定会有一种更优。
- 情况二:如果不全相等,那么肯定存在bp bp+1 bp+2...bq,他们的值相等,
- 那么把他们移到左边的关键点或者右边的关键点,肯定有一种会更优。
- 不断这样移动,最后就变成情况一了。
综上至少存在一种最优方案,最终的所有数都是原来的某个数。
最优解与两个参数有关:当前位置处理后的最大值mx,和当前情况的处理代价值cost。
显然最大值mx最小最好(这样第i+1个值花费代价较小),cost也是最小最好。
用dp[i][j]表示:前i个数构成的序列,这个序列最大值为j,dp[i][j]的值代表相应的cost。
状态转移方程:dp[i][j]=abs(j-w[i])+min(dp[i-1][k]); (k<=j)
这个表格是根据转移方程写出来的 dp 数组。
右边没填充的是因为填充的数字肯定比前面的数字大,无用,
因此,在求 min( dp[ i - 1 ][ k ] ) 时,既然右边更大、则无需考虑。
又从表格中可以看出:这里的 k 无需从1遍历到 j 。
只要在对 j 进行 for 循环的时候不断更新一个 dp[ i - 1 ][ j ] 的最小值:
mn=min(mn,dp[i-1][j]),则 dp[ i ][ j ]=abs( j - w[ i ] ) + mn 。
这样改进之后即可从本来的时候时间复杂度O(NMM)改进为O(NM);
【优化】用离散化思想改进,因为N=2000。远小于 A[ i ] 的最大值。
离散化:将序列排序一下,然后用位置的前后关系来制定其值,这样时间复杂度变成O(N^2)。
#include <bits/stdc++.h> //poj禁用
using namespace std;
typedef long long ll;
typedef unsigned long long ull;
/*【poj 3666】Making the Grade
给出长度为n的整数数列,每次可以将一个数加1或者减1,
最少要多少次可以将其变成不严格的单调递增或者单调递减。 */
//【线性DP】【匹配转移】
//关联因素:当前位置编号(当前最大值编号)mx,和当前情况的代价值cost。
//状态:dp[i][j]表示前i个数,当前最大值编号为j时,相应最小的cost。
//方程:dp[i][j]=abs(j-a[i])+min(dp[i-1][k]); (k<=j)
//优化:for循环记录min(dp[i-1][k]),即mn=min(mn,dp[i-1][j])。
//边界:dp[0][i]=0; 目标:min{dp[n][i]};
//复杂度:O(N^2)(记录mn+离散化之后)
const int N=2005;
int n,a[N],b[N];
long long int dp[N][N];
void solve(){
for(int i=1;i<=n;i++){
ll mn=dp[i-1][1];
for(int j=1;j<=n;j++){
mn=min(mn,dp[i-1][j]);
dp[i][j]=abs(a[i]-b[j])+mn;
}
}
long long ans=dp[n][1];
for(int i=1;i<=n;i++)
ans=min(ans,dp[n][i]);
printf("%lld\n",ans);
}
int main(){
scanf("%d",&n);
for(int i=1;i<=n;i++)
scanf("%d",&a[i]), b[i]=a[i]; //b[i]用于离散化
sort(b+1,b+n+1); //***注意:此处不需要去重***
solve(); return 0;
}
【例题8】noip 2008 传纸条
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
typedef unsigned long long ull;
/*【洛谷 P1006】传纸条
给出M*N(1<=M,N<=50)的矩阵,在这个矩阵内找出两条从1,1到m,n的路径。
(一条从1,1到m,n;一条从m,n到1,1),且路径之上的权值和k最大。
PS:(1,1)和(m,n)权值为0。 */
const int maxn=60;
int a[maxn][maxn];
int F[2*maxn][maxn][maxn];
/* 第一维度维护的是纵坐标与横坐标的和。
=>在同一斜线上 =>结合纵坐标就可以找到确切位置。
第二维度维护的是相对在左边的点的纵坐标。
第三维度维护的是相对在右边的点的纵坐标。*/
//F[sum][i][j]=max{F[sum-1][i][j],F[sum-1][i][j-1],F[sum-1][i-1][j],F[sum-1][i-1][j-1]};
int main(){
int m,n; scanf("%d%d",&m,&n);
for(int i=1;i<=m;i++)
for(int j=1;j<=n;j++) scanf("%d",&a[i][j]);
memset(F,-1,sizeof(F));//赋初值为-1
F[2][1][1]=0; //最初的点,在左上角,好感度为0 [初始化]
for(int k=3;k<m+n;k++) //纵坐标与横坐标的和
for(int i=1;i<n;i++)
for(int j=i+1;j<=n;j++){
int s=F[k][i][j]; //讨论各种情况,更不更新
if(F[k-1][i][j]>s) s=F[k-1][i][j];
if(F[k-1][i-1][j]>s) s=F[k-1][i-1][j];
if(F[k-1][i][j-1]>s) s=F[k-1][i][j-1];
if(F[k-1][i-1][j-1]>s) s=F[k-1][i-1][j-1];
if(s==-1) continue;
//↑↑↑当s为-1时,说明四种情况都不能到该点,故不存在
F[k][i][j]=s+a[k-i][i]+a[k-j][j];
//↑↑↑该点的值为最大的前一个值与当前F[k][i][j]表示两点的值的和
}
printf("%d",F[m+n-1][n-1][n]); //因为i永远小于j,所以右下角的点不会求到
//但是到右下角只有一种情况,在右下角的上面和右下角的左边,直接输出即可
return 0;
}
【例题9】tyvj 1061 移动服务 戳这里
【例题10】SGU 167 I - country 戳这里
【例题11】Cookies 看进阶指南吧Σ( ° △ °|||)
二. 背包DP
(1)0/1背包
问题(0/1背包): 有n个体积和价值分别为vi和wi的物品。
从这些物品中挑出总重量不超过m的物品,求所有挑选方案中价值总和的最大值。
0/1背包:常用于方案数DP,最优值DP。
思想:对于每个物品,比较 [ 放的价值大 ] 还是 [ 不放的价值大 ] 。
转换成方程: f [ i ] [ j ] = Max{ f [ i − 1 ] [ j ] ,f [ i − 1 ] [ j −v [ i ] ] + w [ i ] } ;
【思路1】初步分析
//【背包DP】【01背包】初步分析
//状态:dp[i][j]表示前i个物品、选择体积和为j的、所得的最大价值和。
//方程:dp[i][j]=max{dp[i-1][j](不选i),dp[i-1][j-vi]+wi(选i)};
//初值:dp[0][0]=0; 其余均为负无穷。
//目标:max{dp[n][j]}(0<=j<=m);
memset(dp,0xcf,sizeof(dp)); //-INF
dp[0][0]=0;
for(int i=1;i<=n;i++){
for(int j=0;j<=m;j++) dp[i][j]=dp[i-1][j];
for(int j=v[i];j<=m;j++) //注意:从v[i]开始
dp[i][j]=max(dp[i][j],dp[i-1][j-v[i]]+w[i]);
}
int ans=0;
for(int j=0;j<=m;j++) ans=max(ans,dp[n][j]);
【思路2】滚动数组优化空间
//【背包DP】【01背包】滚动数组
//优化:滚动数组优化,降低空间开销。
//实现:把阶段i的状态存储在第一维下标为i&1的二维数组中。
//当i为奇数时,i&1=1; 当i为偶数时,i&1=0。
//DP的状态相当于在dp[0][],dp[1][]两个数组中交替转移,节省空间。
//状态:dp[i&1][j]表示前i个物品、选择体积和为j的、所得的最大价值和。
//方程:dp[i&1][j]=max{dp[(i-1)&1][j](不选i),dp[(i-1)&1][j-vi]+wi(选i)};
//初值:dp[0][0]=0; 其余均为负无穷。
//目标:max{dp[n&1][j]}(0<=j<=m);
int dp[2][max_m+1];
memset(dp,0xcf,sizeof(dp)); //-INF
dp[0][0]=0;
for(int i=1;i<=n;i++){
for(int j=0;j<=m;j++) dp[i&1][j]=dp[(i-1)&1][j];
for(int j=v[i];j<=m;j++) //注意:从v[i]开始
dp[i&1][j]=max(dp[i&1][j],dp[(i-1)&1][j-v[i]]+w[i]);
}
int ans=0;
for(int j=0;j<=m;j++) ans=max(ans,dp[n&1][j]);
【思路3】转化为一维
转换成一维方程,需要 逆序 。
逆序的目的是:保证后一个 f [ v ] 和 f