刚刚开始学习DP的,请DP大人对我好一点。。(显然没有)
(由于是自学,没有什么定义或者什么简单结论,所以 <我不对我所说的话负法律责任>)
血泪警告:状态以及转移方程一定要自己好好推一遍,而且是要自己出来!!!
学这个的时候就是总看着题解推的,所以现在就基础非常不扎实,以至于快考试了,我还是找不到写DP的感觉,一遇到就爆零,让我难受了好长时间,我决定从头开始好好再刷一遍!!这次要自己推,而且是要自己理解,自己敲代码!!不能看题解!
上题:
线性动态规划
第一题:打鼹鼠
初学DP打完数字三角形后的第一题,,
不是很难,主要看注释。。。
代码:
#include<bits/stdc++.h>
#define sea 11000
using namespace std;
int n,f[sea],m,ans=0;
struct watch{int t,x,y;}a[sea];
bool compare(watch x,watch y){return x.t<y.t;}
int main()
{
cin>>n>>m;
for(int i=1;i<=m;i++)
cin>>a[i].t>>a[i].x>>a[i].y;
sort(a+1,a+n+1,compare);//按时间排序
for(int i=1;i<=m;i++)
{
f[i]=1;//赋初值
for(int j=1;j<i;j++)
if(abs(a[i].x-a[j].x)+abs(a[i].y-a[j].y)<=a[i].t-a[j].t)//关键步骤,如果距离(自己模拟一下就等于跳格子的数量)小于或等于时间间隔,就加入跳格子的次数
f[i]=max(f[j]+1,f[i]);//加入跳格子的次数f[i]
ans=max(f[i],ans);//每次对f[i]取max,就是答案
}
cout<<ans<<endl;
return 0;
}
最长上升子序列
先发个板子:
//LIS
//朴素算法
#include<bits/stdc++.h>
#define LL long long
using namespace std;
const int sea=6000;
int n,a[sea];
int f[sea];//表示到第i个数时的最长上升子序列长度
inline int read()
{
int s=0,w=1;char ch=getchar();
while(ch<'0'||ch>'9'){if(ch=='-')w=-1;ch=getchar();}
while(ch<='9'&&ch>='0')s=s*10+ch-'0',ch=getchar();
return s*w;
}
int main()
{
n=read();
int ans=0;
for(int i=1;i<=n;i++) a[i]=read(),f[i]=1;
for(int i=1;i<=n;i++)
{
for(int j=1;j<i;j++)
if(a[i]>=a[j])
f[i]=max(f[i],f[j]+1);
}
for(int i=1;i<=n;i++) ans=max(ans,f[i]);
cout<<ans;
return 0;
}
//lower_bound二分优化(还是觉得lower_bound比较好写,这个相当手写的于二分查找)
#include<bits/stdc++.h>
using namespace std;
const int sea=600000;
int f[sea];//表示到第i个数时的最长上升子序列
int n,a[sea];
inline int read()
{
int s=0,w=1;char ch=getchar();
while(ch<'0'||ch>'9'){if(ch=='-')w=-1;ch=getchar();}
while(ch<='9'&&ch>='0')s=s*10+ch-'0',ch=getchar();
return s*w;
}
int main()
{
n=read();
for(int i=1;i<=n;i++) a[i]=read();
f[1]=a[1];
int len=1;
for(int i=2;i<=n;i++)
{
if(a[i]>f[len]) f[++len]=a[i];
else
{
int j=lower_bound(f+1,f+len+1,a[i])-f;
//lower_bound是返回在f[]数组中从1开始f+len+1之前的数中最大的<=a[i]的数的下标是什么
f[j]=a[i];
}
}
//最后的答案就自认存在了len中
// for(int i=1;i<=n;i++) cout<<f[i]<<' ';
// f[i]也就是最长上升子序列中的一种
printf("%d",len);
return 0;
}
例题:导弹拦截
//O(n^2)
#include<bits/stdc++.h>
#define over(i,s,t) for(int i=s;i<=t;i++)
#define lver(i,t,s) for(int i=t;i>=s;i--)
using namespace std;
inline int read()
{
int s=0,w=1; char ch=getchar();
while(ch<'0'||ch>'9'){if(ch=='-')w=-1;ch=getchar();}
while(ch<='9'&&ch>='0')s=s*10+ch-'0',ch=getchar();
return s*w;
}
const int sea=210;
int n,g[sea],f[sea],a[sea],ans1,ans2;//这个状态存的是到达i之前的最大不上升子序列的长度
int main()
{
n=read(); over(i,1,n) a[i]=read(),f[i]=g[i]=1;
over(i,1,n) over(j,1,i-1) if(a[i]<=a[j]) f[i]=max(f[i],f[j]+1);
over(i,1,n) over(j,1,i-1) if(a[i]>a[j]) g[i]=max(g[i],g[j]+1);
over(i,1,n) ans1=max(f[i],ans1),ans2=max(g[i],ans2); printf("%d\n%d\n",ans1,ans2);
return 0;
}
//O(nlogn)
#include<bits/stdc++.h>
#define over(i,s,t) for(int i=s;i<=t;i++)
#define lver(i,t,s) for(int i=t;i>=s;i--)
using namespace std;
inline int read()
{
int s=0,w=1; char ch=getchar();
while(ch<'0'||ch>'9'){if(ch=='-')w=-1;ch=getchar();}
while(ch<='9'&&ch>='0')s=s*10+ch-'0',ch=getchar();
return s*w;
}
const int sea=210;
int n,len=0,a[sea],f[sea];//这个状态存的是逆序的a[]数组
void ef1(int x)
{
int l=1,r=len;
while(l<r){int mid=(l+r)>>1;if(x>f[mid]) r=mid; else l=mid+1;}
f[l]=x;
}
void ef2(int x)
{
int l=1,r=len;
while(l<r){int mid=(l+r)>>1;if(x<=f[mid]) r=mid; else l=mid+1;}
f[l]=x;
}
int main()
{
scanf("%d",&n); over(i,1,n)scanf("%d",&a[i]),f[i]=1;
over(i,1,n) if(len==0||a[i]<=f[len]) f[++len]=a[i];else ef1(a[i]); printf("%d\n",len);len=0;
over(i,1,n) if(a[i]>f[len]) f[++len]=a[i]; else ef2(a[i]); printf("%d\n",len);
return 0;
}
第二题: 尼克船长
题解:
两种状态:
1.当他可以闲着的时候:f[i]=f[i+1]+1;
2.当他有空的时候:f[i]=max(f[i],f[i+a[num].s]);
代码:
#include<bits/stdc++.h>
#define sea 10001
#define ll long long //不用想就要开long long
using namespace std;
ll n,m,t,s,sum[sea],f[sea];
inline int read()
{
int s=0,w=1; char ch=getchar();
while(ch<'0'||ch>'9'){if(ch=='-')w=-1; ch=getchar();}
while(ch>='0'&&ch<='9')s=s*10+ch-'0',ch=getchar();
return s*w;
}
struct see{ll t,s;}a[sea];
bool compare(see a,see b){return a.t>b.t;}
int main()
{
n=read(); m=read();
for(ll i=1;i<=m;i++)
{
a[i].t=read(); a[i].s=read();
sum[a[i].t]++;//记下有工作的时间
}
sort(a+1,a+m+1,compare);//倒着排
int num=1;
for(ll i=n;i>=1;i--)//倒着搜
{
if(sum[i]==0) f[i]=f[i+1]+1;//没有任务空闲的时候
else
for(ll j=1;j<=sum[i];j++)//有任务的时候(存在多重任务的时候)
{
f[i]=max(f[i],f[i+a[num].s]);//状态转移方程,具体解释看上方
num++;//就是个计数器。。。
}
}
cout<<f[1]<<endl;
return 0;
}
最长公共子序列
第三题:编译距离
一道很好的板子题 ,
还是两个状态:
1.如果两个字母一样,f[i][j]=f[i-1][f-1];
2.如果两个字母不一样的话三种情况取最小值
动态转移方程:
f[i][j]=min(f[i-1][j-1],min(f[i-1][j],f[i][j-1]))+1;
代码:
#include<bits/stdc++.h>
#define sea 2010
using namespace std;
int n,m,f[sea][sea];
char a[sea],b[sea];
int main()
{
scanf("%s %s",a,b);
m=strlen(a); n=strlen(b);
for(int i=1;i<=m;i++) f[i][0]=i;
for(int j=1;j<=n;j++) f[0][j]=j;
for(int i=1;i<=m;i++)
for(int j=1;j<=n;j++)
{
if(a[i-1]==b[j-1])//第一状态(由于a,b是字符串从0开始的所以减一)
{
f[i][j]=f[i-1][j-1];
continue;
}
f[i][j]=min(f[i-1][j-1],min(f[i-1][j],f[i][j-1]))+1;//第二状态(分别为:更改,插入,删除)
}
printf("%d",f[m][n]);
return 0;
}
合并型DP:
第四题:石子合并
一道杠了很长时间的板子题,向大神请教了好长时间,所以打算好好讲一下(并不是说我之前就没有好好讲!!!)
首先对一个环中得两块石头进行合并的时候,分三个循环走:
1.整个环
2.环上的起始点
3.状态转移点
最重要的状态转移方程:
x[i][j]=min(x[i][j],x[i][k]+x[k+1][j]+sum[j]-sum[i-1]);//最小值
比较x[i][k]和x[k+1][j]之间是否对答案有贡献,然后加上它们的前缀和(这里的sum[j]-sum[i-1]如果不懂的话,看下图)
(字太丑了,对不住了,就凑乎看吧)
最大值也一样:
y[i][j]=max(y[i][j],y[i][k]+y[k+1][j]+sum[j]-sum[i-1]);//最大值
代码:
#include<bits/stdc++.h>
#define sea 5000
#define ocean 1100000
using namespace std;
int n,x[sea][sea],y[sea][sea],a[ocean],sum[ocean];
inline int read()
{
int s=0,w=1; char ch=getchar();
while(ch<'0'||ch>'9'){if(ch=='-')w=-1; ch=getchar();}
while(ch<='9'&&ch>='0')s=s*10+ch-'0',ch=getchar();
return s*w;
}
int main()
{
n=read();
for(int i=1;i<=n;i++)
{
a[i]=read();
a[i+n]=a[i];//处理环
}
for(int i=1;i<=n*2;i++)
x[i][i]=0,y[i][i]=0,sum[i]=sum[i-1]+a[i];//xy的处理及前缀和
for(int len=2;len<=n;len++)//循环 环
{
for(int i=1;i<=n*2-len+1;i++)//循环 起始点
{
int j=len+i-1;
x[i][j]=0x3f;y[i][j]=-1;//赋初值
for(int k=i;k<=j-1;k++)//循环 转移点
{
x[i][j]=min(x[i][j],x[i][k]+x[k+1][j]+sum[j]-sum[i-1]);//最小值
y[i][j]=max(y[i][j],y[i][k]+y[k+1][j]+sum[j]-sum[i-1]);//最大值
}
}
}
int ans1=0x3f,ans2=0;
for(int i=1;i<=n;i++)
{
ans1=min(ans1,x[i][i+n-1]);//最小答案
ans2=max(ans2,y[i][i+n-1]);//最大答案
}
printf("%d %d",ans1,ans2);
return 0;
}
当然这是个赤裸裸的区间dp(n3)算法,现在就是展现新算法的时候了!!!(好草的开场白)
石子合并平行四边形优化
解法明天明天明天准时,,,
代码:
#include<iostream>
#include<cstring>
#define sea 2015
using namespace std;
int n,a[sea],dp[sea][sea],sum[sea],s[sea][sea];
inline int read()
{
int s=0,w=1;char ch=getchar();
while(ch<'0'||ch>'9'){if(ch=='-')w=-1; ch=getchar();}
while(ch<='9'&&ch>='0') s=s*10+ch-'0',ch=getchar();
return s*w;
}
int main()
{
n=read();
sum[0]=0;
memset(dp,0x3f,sizeof(dp));
for(int i=1;i<=n;i++)
{
a[i]=read();
sum[i]=sum[i-1]+a[i];
s[i][i]=i,dp[i][i]=0;
}
for(int i=1;i<n;i++)
{
sum[i+n]=sum[i+n-1]+a[i];
s[i+n][i+n]=i+n,dp[i+n][i+n]=0;
}
for(int len=2;len<=n;len++)
{
for(int i=1;i<=2*n-1;i++)
{
int j=i+len-1;
if(j>2*n-1) break;
for(int k=s[i][j-1];k<=s[i+1][j];k++)
if(dp[i][j]>dp[i][k]+dp[k+1][j]+sum[j]-sum[i-1])
dp[i][j]=dp[i][k]+dp[k+1][j]+sum[j]-sum[i-1],s[i][j]=k;//最小值,由于最大值不具有单调性所以用四边形不等式无法进行
}
}
int ans=0x3fffffff;
for(int i=1;i<=n;i++)
ans=min(ans,dp[i][n+i-1]);
printf("%d\n",ans);
return 0;
}
枚举型动态规划
第五题: 滑雪课
一个非常有意思的暴力题,,,
题解:
最重要的事放第一位:能力值最大是100,最多有100节滑雪课(这也是枚举型动态规划的前提条件(至少要跑的过啊))
先说一下最最最最重要的预处理:
一个for循环直接处理出imp[i]就是能力值最小的时去滑雪的最短时间(便于到dp中的先滑雪的优化情况)
然后就是次重要的c[i][j]数组:
记录c[这节课开始的时间][第几节课]=这个时间有几节课,这样记录之后就可以推出:
l[c[i][k]]:就是从i时间点开始的第k节课的时间长度
a[c[i][k]]:就是从i时间点开始的第k节课之后变为的能力值
然后就是最简单的 DP了,先全部处理成-1,便于之后的跳过,然后就分为三种状态去优化即可
(f[i][j]表示时刻i能力值为j时能够完成的最多滑雪次数 )
1.休息:f[i+1][j]=max(f[i+1][j],f[i][j]);
2.滑雪(一定要先滑雪在上课,可以推一下样例例如下面推的样例图):f[i+imp[j]][j]=max(f[i+imp[j]][j],f[i][j]+1);
3.上课:
先上个循环找到每节课的能力值和时间长度然后再优化:
f[i+tmp][pow]=max(f[i+tmp][pow],f[i][j]);
最后答案就是1到100之间的最大滑雪时间即可。
代码:
#include<bits/stdc++.h>
#define sea 11000
using namespace std;
struct watch{int power,time;}snow[10001];
int t,s,n,a[101],m[101],l[101],f[10001][101],c[10001][101],imp[101];
inline int read()
{
int s=0,w=1; char ch=getchar();
while(ch<'0'||ch>'9'){if(ch=='-')w=-1;ch=getchar(); }
while(ch<='9'&&ch>='0')s=s*10+ch-'0',ch=getchar();
return s*w;
}
//能力值最大是100,最多有100节滑雪课
void yuchuli()
{
for(int i=1;i<=100;i++)
{
imp[i]=11000;//先赋为最大值
for(int j=1;j<=n;j++)
if(snow[j].power<=i)//选出最小的能力
imp[i]=min(snow[j].time,imp[i]);//选出所需时间最小的的雪坡
}
}
int main()
{
t=read(); s=read(); n=read();
for(int i=1;i<=s;i++)
{
m[i]=read(); l[i]=read(); a[i]=read();
c[m[i]][0]++; c[m[i]][c[m[i]][0]]=i;
}
for(int i=1;i<=n;i++) snow[i].power=read(),snow[i].time=read();
yuchuli();
for(int i=0;i<=t;i++)
for(int j=1;j<=100;j++)
f[i][j]=-1;
f[0][1]=0;
for(int i=0;i<=t;i++)
{
for(int j=1;j<=100;j++)
{
if(f[i][j]==-1) continue;
f[i+1][j]=max(f[i+1][j],f[i][j]);//休息
f[i+imp[j]][j]=max(f[i][j]+1,f[i+imp[j]][j]);//自己滑雪
for(int k=1;k<=c[i][0];k++)//上课
{
int tm=l[c[i][k]];//这门课所持续的时间
int pw=a[c[i][k]];//上完这门课能够到达的能力值
f[i+tm][pw]=max(f[i+tm][pw],f[i][j]);
}
}
}
int ans=0;
for(int i=1;i<=100;i++) ans=max(f[t][i],ans);
printf("%d",ans);
return 0;
}
规则类动态规划
第六题:传纸条
就像是左边的大神所说的,用图论就能写,dfs就能过的简题,但是为了好好的学我的DP,就决定好好去写一下DP:
不算太难打的一道题,但是快读困了我好长时间,但是换成scanf就过了,(是我快读出错了吗??|墙|ョ゚ェ゚;))
还是状态转移:当传纸条的时候,有四种情况:
f[i][j][k]可以表示为[横纵坐标的和][此点的左边点的纵坐标][此点的右边点的纵坐标]
1.可以不传,还在A: f[i-1][j][k]
2.可以传到B: f[i-1][j][k-1]
3.可以传到C: f[i-1][j-1][k]
4.可以传到D: f[i-1][j-1][k-1]
代码:
#include<bits/stdc++.h>
#define sea 101
using namespace std;
int n,m,a[sea][sea],f[sea][sea][sea];
int main()
{
scanf("%d%d",&n,&m);
for(int i=1;i<=n;i++)
for(int j=1;j<=m;j++)
scanf("%d",&a[i][j]);
f[2][1][1]=0;
for(int i=3;i<n+m;i++)
for(int j=1;j<=n;j++)
for(int k=j+1;k<=n;k++)
{
if(m<=i-j&&i+j<=1&&m<=i-k&&i+k<=1) continue;//不出界,就像是搜索一样
f[i][j][k]=max(f[i-1][j][k],max(f[i-1][j-1][k-1],max(f[i-1][j-1][k],f[i-1][j][k-1])));
f[i][j][k]+=a[j][i-j]+a[k][i-k];
}
int ans=f[n+m-1][n-1][n]+a[n][m];
printf("%d",ans);
return 0;
}
环形DP
第七题: 多边形游戏
经典的区间DP的变形,最重要的一点就是你要知道负负得正即可。
直接上状态转移方程式:
加法:
1、f[i][j][1]=max(f[i][j][1],f[i][k][1]+f[k+1][j][1]);
2、f[i][j][0]=min(f[i][j][0],f[i][k][0]+f[k+1][j][0]);
乘法:
1、正正
2、正负
3、负正
4、负负
(直接取(12334)最大值和最小值即可)
代码:
#include<iostream>
#define sea 300
#define ocean 32768
#define rain -32768//特别好理解的最大小值
using namespace std;
int n,a[sea],f[sea][sea][2];
char cot[sea];
int main()
{
cin>>n;int t;
for(int i=1;i<=n;i++)
{
t=i-1;
if(t==0) t=n;//处理运算符的下表
//处理运算符的环
cin>>cot[t];
cot[t+n]=cot[t];
//处理点的环
cin>>f[i][i][1];
f[i][i][0]=f[i][i][1];
f[i+n][i+n][1]=f[i][i][1];
f[i+n][i+n][0]=f[i][i][1];
}
for(int len=1;len<n;len++)//经典的区间DP做法
for(int i=1;i<=n+n;i++)
{
int j=i+len;
f[i][j][1]=rain;
f[i][j][0]=ocean;
for(int k=i;k<j;k++)
{
if(cot[k]=='t')//加法
{
f[i][j][1]=max(f[i][j][1],f[i][k][1]+f[k+1][j][1]);
f[i][j][0]=min(f[i][j][0],f[i][k][0]+f[k+1][j][0]);
}
else//乘法
{
int a,b,c,d;
a=f[i][k][0]*f[k+1][j][0];
b=f[i][k][1]*f[k+1][j][0];
c=f[i][k][1]*f[k+1][j][1];
d=f[i][k][0]*f[k+1][j][1];
f[i][j][1]=max(f[i][j][1],max(max(a,b),max(c,d)));
f[i][j][0]=min(f[i][j][0],min(min(a,b),min(c,d)));
}
}
}
int ans=rain;
for(int i=1;i<=n;i++)
ans=max(ans,f[i][i+n-1][1]);//找出最大值
cout<<ans<<endl;
for(int i=1;i<=n;i++)
if(f[i][n+i-1][1]==ans)//找出断开点
cout<<i<<' ';
return 0;
}
资源分配类DP
第八题: 花店橱窗布置
先%%%%%%一下石神tqltqltql
在石神的细心讲解下,本菜鸡终于参透了这种类型的DP了,,,(高兴地飞了起来)
这道题就是典型的资源分配DP,就是把几种花分到几个花瓶中,最重要的是要把顺序或者是每种花如何分配的输出来,这就不是很好想了。。
还是先写转移方程:
f[i][j]表示:[前i种花朵][第j个花瓶]=最大美学值
f[i][j]=max(f[i][j-1],f[i-1][j-1]+a[i][j]);
但是要记录每次的f[i][j]给怎么办呢???
再次%%%%石神,再开个新数组:g[i][j].x表示i,g[i][j].y表示j即可;
然后就是最重要的递归输出:
其实每次++ss即可;
最后记得倒序输出哦,,
#include<bits/stdc++.h>
#define sea 520
using namespace std;
int n,m,a[sea][sea],f[sea][sea],ans[sea],ss=0;;
struct solu
{
int x,y;
}g[sea][sea];
void pri(int i,int j)
{
if(!j) return ;
int x=g[i][j].x,y=g[i][j].y;
if(x<i) ans[++ss]=j;
pri(x,y);
}
int main()
{
cin>>n>>m;
for(int i=1;i<=n;i++)
for(int j=1;j<=m;j++)
cin>>a[i][j];
for(int i=0;i<=n;i++)
for(int j=0;j<=m;j++)
f[i][j]=-0x3f;
for(int i=0;i<=m;i++)
f[0][i]=0;
for(int i=1;i<=n;i++)
{
for(int j=i;j<=m;j++)
{
if(f[i][j-1]>=f[i-1][j-1]+a[i][j])
f[i][j]=f[i][j-1],g[i][j].x=i,g[i][j].y=j-1;
else
f[i][j]=f[i-1][j-1]+a[i][j],g[i][j].x=i-1,g[i][j].y=j-1;
}
}
printf("%d\n",f[n][m]);
pri(n,m);
for(int i=ss;i>=1;i--)
printf("%d ",ans[i]);
return 0;
}
双倍经验:
#include<bits/stdc++.h>
#define sea 120
using namespace std;
int n,m,a[sea][sea],f[sea][sea],xx;
int print(int i,int j)
{
if(i==0) return 0;
for(int k=0;k<=j;k++)
{
if(f[i-1][k]+a[i][j-k]==xx)
{
xx=f[i-1][k];
print(i-1,k);
cout<<i<<' '<<j-k<<endl;
break;
}
}
}
int main()
{
cin>>n>>m;
for(int i=1;i<=n;i++)
for(int j=1;j<=m;j++)
cin>>a[i][j];
memset(f,0,sizeof(f));
for(int i=1;i<=n;i++)
for(int j=1;j<=m;j++)
{
xx=0;//用这个记录即可(相当于上面的g[i][j])
for(int k=0;k<=j;k++)
{
xx=max(xx,f[i-1][k]+a[i][j-k]);
f[i][j]=xx;
}
}
cout<<f[n][m]<<endl;
print(n,m);
return 0;
}
区间DP
第九题:加分二叉树
非常有意思的题,巧妙的将树和DP联系在一起的DP题,没有很难但是很练习基本功。
先复习一下树的前序,中序,后序
前序: 先根再左右子树——1 3 2
中序: 先左再中最后右——3 1 2
后序: 先左右子树再根——3 2 1
此题共有两问,可以分开处理
第一问:DP求最大值:
典型的区间DP,直接上状态转移方程了:
if(f[i][j]<f[i][k-1]*f[k+1][j]+f[k][k])
f[i][j]=f[i][k-1]*f[k+1][j]+f[k][k],root[i][j]=k;
记录下来root[i][j]方便查找前序;
第二问:求前序:
print直接递归就好了,代码很容易懂的;
#include<bits/stdc++.h>
#define sea 500
using namespace std;
int n,f[sea][sea],root[sea][sea];
void print(int l,int r)
{
if(l>r) return ;
cout<<root[l][r]<<' ';
if(l==r) return ;
print(l,root[l][r]-1);
print(root[l][r]+1,r);
}
int main()
{
cin>>n;
for(int i=1;i<=n;i++)
{
cin>>f[i][i];
f[i][i-1]=1;
root[i][i]=i;
}
for(int len=1;len<n;len++)
{
for(int i=1;i+len<=n;i++)
{
int j=len+i;
f[i][j]=f[i+1][j]+f[i][i];
root[i][j]=i;
for(int k=i+1;k<j;k++)
{
if(f[i][j]<f[i][k-1]*f[k+1][j]+f[k][k])
{
f[i][j]=f[i][k-1]*f[k+1][j]+f[k][k];
root[i][j]=k;
}
}
}
}
cout<<f[1][n]<<endl;
print(1,n);
return 0;
}
学了一个星期多的零乱线性DP,整体觉得还算都通了一遍,下面就该学最最最最最最经典的背包了,(觉得背包会好难)。。。\( T﹏T )/