上讲习题
AcWing 482
这个题和上一讲的AcWing 1014没有任何区别。出队的越少留下的就越多,用 n n n减去最长的一个上升又下降的子序列即可。
#include<bits/stdc++.h>
using namespace std;
const int NN=1004;
int a[NN],f1[NN],f2[NN];
int main()
{
int n;
scanf("%d",&n);
for(int i=1;i<=n;i++)
scanf("%d",&a[i]);
int ans=0;
for(int i=1;i<=n;i++)
{
f1[i]=1;
for(int j=1;j<=i-1;j++)
if(a[j]<a[i])
f1[i]=max(f1[i],f1[j]+1);
}
for(int i=n;i>=1;i--)
{
f2[i]=1;
for(int j=i+1;j<=n;j++)
if(a[j]<a[i])
f2[i]=max(f2[i],f2[j]+1);
}
for(int i=1;i<=n;i++)
ans=max(f1[i]+f2[i]-1,ans);
printf("%d\n",n-ans);
return 0;
}
AcWing 1016
这个题从原来的求最长上升子序列变成了求和最大的上升子序列。之前新加一个数 a i a_i ai子序列的长度长度加 1 1 1,现在和却增加了 a i a_i ai,所以状态转移方程就变成了 max ( f j , a j < a i , j < i ) + a i \max(f_j,a_j<a_i,j<i)+a_i max(fj,aj<ai,j<i)+ai,只用自己(边界条件)也是 f i = a i f_i=a_i fi=ai。
#include<bits/stdc++.h>
using namespace std;
const int NN=1004;
int a[NN],f[NN];
int main()
{
int n,ans=0;
scanf("%d",&n);
for(int i=1;i<=n;i++)
{
scanf("%d",&a[i]);
f[i]=a[i];
for(int j=1;j<i;j++)
if(a[j]<a[i])
f[i]=max(f[i],f[j]+a[i]);
ans=max(ans,f[i]);
}
printf("%d",ans);
return 0;
}
AcWing 187
这个题目可以考虑上讲中拦截导弹那道题的贪心算法。但是这个题目可以选择上升或者下降,贪心无法在新加一个时确定是上升还是下降。于是我们再分析,发现题目中 n n n很小,不难想到可以暴搜。每次两种决策,在下降的选一个或者新加,在上升的选一个或者新加。这样分成了两种,就可以分别贪心了。注意,如果当前用的个数大于答案就可以退出了,因为最小的一定会是这个方案。用 u u u表示上升的序列的个数, d d d表示下降, t t t表示处理第几个。
#include<bits/stdc++.h>
using namespace std;
const int NN=54;
int a[NN],up[NN],down[NN],n,ans;
void dfs(int u,int d,int t)
{
if(u+d>=ans)
return;
if(t==n)
{
ans=u+d;
return;
}
int k=u+1;
for(int i=1;i<=u;i++)
if(up[i]<a[t])
{
k=i;
break;
}
int temp=up[k];
up[k]=a[t];
dfs(max(u,k),d,t+1);
up[k]=temp;
k=d+1;
for(int i=1;i<=d;i++)
if(down[i]>a[t])
{
k=i;
break;
}
temp=down[k];
down[k]=a[t];
dfs(u,max(d,k),t+1);
down[k]=temp;
}
int main()
{
while(scanf("%d",&n)!=EOF&&n)
{
ans=1e9;
for(int i=0;i<n;i++)
scanf("%d",&a[i]);
dfs(0,0,0);
printf("%d\n",ans);
}
return 0;
}
01背包
概念
有一个背包,最大容量是 m m m,有很多物品,重量是 w i w_i wi,价值是 c i c_i ci,要求把东西装进背包里且不超过最大容量能获得的最大价值是多少。
方法
这种可以定义 f i , j f_{i,j} fi,j为用 1... i 1...i 1...i的物品最多装 j j j的方案。首先,每个状态都可以一个不装,所以初始化为 0 0 0。如果题目要求恰好装多少,则只有最多装 0 0 0可以一个都不装,初始化为 0 0 0。然后, f i , j f_{i,j} fi,j可以不用 i i i, f i , j = f i − 1 , j f_{i,j}=f_{i-1,j} fi,j=fi−1,j,也可以用第 i i i个,则能得到 c i c_i ci的钱, f i , j = f i − 1 , j − w i + c i f_{i,j}=f_{i-1,j-w_i}+c_i fi,j=fi−1,j−wi+ci。然后我们发现,所有的 f i , j f_{i,j} fi,j都依赖于 f i − 1 f_{i-1} fi−1的数。所以,可以滚动数组,从后往前枚举 j j j,因为状态转移用的 k k k都更小,这样就在存的 f i − 1 , k f_{i-1,k} fi−1,k被更新成 f i , k f_{i,k} fi,k之前把 f i , j f_{i,j} fi,j计算了。
例题
AcWing 423
这个题是典型的 01 01 01背包。把总的采药时间看成背包的容量,采一个药的时间看成物品的重量。
#include<bits/stdc++.h>
using namespace std;
const int NN=104;
int f[1004],w[NN],c[NN];
int main()
{
int t,m;
scanf("%d%d",&t,&m);
for(int i=1;i<=m;i++)
scanf("%d%d",&w[i],&c[i]);
for(int i=1;i<=m;i++)
for(int j=t;j>=w[i];j--)
f[j]=max(f[j-w[i]]+c[i],f[j]);
printf("%d",f[t]);
return 0;
}
AcWing 734
这个题目发现每分钟会有价值流失,所以必须要决定顺序。首先想到的是暴力枚举顺序,但是发现时间完全不够。所以考虑寻找特点,设两个石头 x , y x,y x,y,先用 x x x和先用 y y y少去的能量的比为 ( s x × l y ) : ( s y × l x ) (s_x\times l_y):(s_y\times l_x) (sx×ly):(sy×lx),则只有左边得到的值比右边的大先后顺序就要交换。于是,决定了顺序就是一个背包了。我们为了方便计算少去的能量,设 f j f_j fj为刚好用 j j j的时间得到的最大能量。则按前面的来说,要先设为负无穷,只有 f 0 f_0 f0设为 0 0 0。答案就是所有时刻结束的最大值。需要注意的是,一个石头不可能变成负的能量。那么少去的能量呢?最后用的第 i i i个,少了 j − s i j-s_i j−si分钟的能量,乘上每分钟少的值即可。
#include<bits/stdc++.h>
using namespace std;
struct node
{
int s,e,l;
bool operator<(const node&it)const
{
return s*it.l<it.s*l;
}
}stone[104];
int f[10004];
int main()
{
int t;
scanf("%d",&t);
for(int kase=1;kase<=t;kase++)
{
int n,m=0;
scanf("%d",&n);
for(int i=1;i<=n;i++)
{
scanf("%d%d%d",&stone[i].s,&stone[i].e,&stone[i].l);
m+=stone[i].s;
}
sort(stone+1,stone+1+n);
memset(f,-0x3f,sizeof(f));
f[0]=0;
for(int i=1;i<=n;i++)
for(int j=m;j>=stone[i].s;j--)
f[j]=max(f[j],f[j-stone[i].s]+max(0,stone[i].e-stone[i].l*(j-stone[i].s)));
int ans=0;
for(int i=0;i<=m;i++)
ans=max(ans,f[i]);
printf("Case #%d: %d\n",kase,ans);
}
return 0;
}
完全背包
概念
和 01 01 01背包的概念完全一样,就是可以用的物品个数变成了无限的。
方法
注意,完全背包是可以用多个的,所以在更新我之前可以先把之前的 f f f用一个该物品更新,所以二维数组改成用 f i f_i fi更新,滚动数组也是从小到大枚举。
例题
AcWing 532
这个题不难发现,如果能被几个面值的钱表示出来的钱是完全没有必要存在的。那么是不是只有这些面值的能取掉呢?答案是肯定的。至于证明我不会,有大佬会的私信或者评论区留言,我会第一时间回复并修改的。回到题目,首先一个小的是肯定不能被更大的表示出来的,所以可以从小到大枚举,如果所有更小的都没有覆盖它那么就必须留下了,然后去表示别人。至于表示怎么弄,只要一个面值在之前可以被表示出来,那么我们再加上一个这个面值的货币肯定也是能够表示的。因为货币是无限的,所以像完全背包一样从小到大枚举并表示。
#include<bits/stdc++.h>
using namespace std;
const int NN=50004;
int vis[NN],a[104];
int main()
{
int t;
scanf("%d",&t);
while(t--)
{
memset(vis,false,sizeof(vis));
int n,ans=0;
scanf("%d",&n);
for(int i=1;i<=n;i++)
scanf("%d",&a[i]);
sort(a+1,a+n+1);
for(int i=1;i<=n;i++)
{
if(vis[a[i]])
continue;
ans++;
vis[a[i]]=true;
for(int j=1;j<=a[n];j++)
if(vis[j])
vis[j+a[i]]=true;
}
printf("%d\n",ans);
}
return 0;
}
多重背包
概念
和 01 01 01背包也一样,但是每个物品有多个且有使用数量限制。
方法
法一
可以想到一种方法,把这个拆成多个只能用一个的物品,改用 01 01 01背包求,但是时间复杂度较高,拆出来的个数都是 n × s n\times s n×s个,再加上背包,很多题是不够用的。
法二
考虑在上面优化,我们知道,每一个数都可以拆成一个二进制表达式,比如 13 = 1101 = 1 + 4 + 8 13=1101=1+4+8 13=1101=1+4+8,所以我们可以考虑把一个的个数拆成这样 1 , 2 , 4 , 8... 1,2,4,8... 1,2,4,8...的形式,可以把每一个数表示出来,如买 13 13 13个就相当于买 1 1 1个、 2 2 2个打包和 8 8 8个打包的东西。如果 1 , 2 , 4 , 8... 1,2,4,8... 1,2,4,8...这样加下来有剩余没办法再打包的就再打一个包,这样可以用前面能表达的加上该包得到之前不能表示的 2 k + 1 2^k+1 2k+1到 s i s_i si。要拆出来 n × log s n\times \log s n×logs个左右。
法三
虽然这样拆会快很多,那万一 m m m(背包容量)很大呢?只能考虑不用 01 01 01背包。我们可以发现, f j f_j fj最多只会用 f j − s × v f_{j-s\times v} fj−s×v更新,而且只会用最大值。于是我们惊奇地发现,这不就是滑动窗口吗?于是我们就可以用单调队列解决。但是我们发现,每加上一个 v v v,对应的 w w w也要多加一个,所以我们在入队的时候可以减去 k × w k\times w k×w,计算的时候直接加上 k × w k\times w k×w。注意,这里两个 k k k是在循环实时更新的, k k k的值不一样。最后,我们注意,这样每次加一个 v v v会有一些不会更新,但是发现通过余数刚好分成了 v v v组,所以还要循环 v v v种余数。
例题
AcWing 1019
这个题范围都很小,直接按第一种方法做即可。
#include<bits/stdc++.h>
using namespace std;
const int NN=5004;
int f[6004],v[NN],w[NN];
int main()
{
int n,m,x=0;
scanf("%d%d",&n,&m);
for(int i=1;i<=n;i++)
{
int k,p,s;
scanf("%d%d%d",&k,&p,&s);
for(int j=1;j<=s;j++)
{
v[++x]=k;
w[x]=p;
}
}
for(int i=1;i<=x;i++)
for(int j=m;j>=v[i];j--)
f[j]=max(f[j-v[i]]+w[i],f[j]);
printf("%d",f[m]);
return 0;
}
AcWing 6
这个题因为 m m m(在本题为 v v v)的范围很大,所以直接按第三种方法做即可。
#include<bits/stdc++.h>
using namespace std;
const int NN=20004;
int f[NN],g[NN],q[NN];
int main()
{
int n,m;
scanf("%d%d",&n,&m);
for(int i=1;i<=n;i++)
{
int v,w,s;
scanf("%d%d%d",&v,&w,&s);
memcpy(g,f,sizeof(f));
for(int j=0;j<v;j++)
{
int h=0,t=-1;
for(int k=j;k<=m;k+=v)
{
if(h<=t&&k-v*s>q[h])
h++;
if(h<=t)
f[k]=max(f[k],g[q[h]]+(k-q[h])/v*w);
while(h<=t&&g[q[t]]-q[t]/v*w<=g[k]-k/v*w)
t--;
q[++t]=k;
}
}
}
printf("%d",f[m]);
return 0;
}
混合背包
概念
上述的多个背包模型混在一起。
方法
分成两个情况分别讨论即可。完全背包就做完全背包, 01 01 01背包和多重背包放在一起即可。
例题
AcWing 7
这个题目就是个混合背包的板子。注意本题多重背包的范围较大,要用第二种方法。
#include<bits/stdc++.h>
using namespace std;
const int NN=1004;
int f[NN],w[NN*NN],c[NN*NN];
bool ok[NN*NN];
int main()
{
int n,m;
scanf("%d%d",&n,&m);
for(int i=1;i<=n;i++)
{
int v,w,s;
scanf("%d%d%d",&v,&w,&s);
if(s)
{
if(s==-1)
s=1;
for(int k=1;k<=s;k*=2)
{
for(int j=m;j>=k*v;j--)
f[j]=max(f[j],f[j-v*k]+w*k);
s-=k;
}
if(s)
for(int j=m;j>=s*v;j--)
f[j]=max(f[j],f[j-s*v]+s*w);
}
else
for(int j=v;j<=m;j++)
f[j]=max(f[j-v]+w,f[j]);
}
printf("%d",f[m]);
return 0;
}
二维费用背包
概念
背包问题的限制变成了两个,体积和重量。
方法
那么就设 f i , j f_{i,j} fi,j为第一个限制最多用 i i i,第二个最多 j j j的最大收益。恰好的同理。用了两个得到收益,状态转移方程 f i , j = max ( f i , j , f i − x , j − y + c ) f_{i,j}=\max(f_{i,j},f_{i-x,j-y}+c) fi,j=max(fi,j,fi−x,j−y+c)
例题
AcWing 8
这个题直接套板子即可。
#include<bits/stdc++.h>
using namespace std;
const int NN=1004;
int f[NN][NN],a[NN],b[NN],c[NN];
int main()
{
int k,n,m;
scanf("%d%d%d",&k,&n,&m);
for(int i=1;i<=k;i++)
{
int a,b,c;
scanf("%d%d%d",&a,&b,&c);
for(int j=n;j>=a;j--)
for(int x=m;x>=b;x--)
f[j][x]=max(f[j][x],f[j-a][x-b]+c);
}
printf("%d",f[n][m]);
return 0;
}
分组背包
概念
将很多物品分成多组,每组中只能选一个装进背包。求最大收益。
方法
用 f i , j f_{i,j} fi,j表示前 i i i组最大用 j j j个的方案。首先可以不选,状态转移: f i , j = f i − 1 , j f_{i,j}=f_{i-1,j} fi,j=fi−1,j,也可以选一个, f i , j = max ( f i − 1 , j − v + w ) f_{i,j}=\max(f_{i-1,j-v}+w) fi,j=max(fi−1,j−v+w)。当然,也可以像 01 01 01背包一样用滚动数组。
例题
AcWing 1013
这个题目一个公司只能选择一种机器的数量,所以可以看成一组中的物品。总机器数看成背包的大小,用的个数看成重量。于是我们考虑如何输出方案。因为最优方案一定会用完所有的机器,所以先记录 j j j为机器数,每次看看是从哪个迭代的,设是从 f i , j − k f_{i,j-k} fi,j−k迭代的,则该公司就用了 k k k个机器,剩余的机器也只有 j − k j-k j−k个了。因为本题要用到前面的 f f f,所以本题就不能滚动数组。
#include<bits/stdc++.h>
using namespace std;
const int NN=14,MM=20;
int w[NN][MM],f[NN][MM],ans[NN];
int main()
{
int n,m;
scanf("%d%d",&n,&m);
for(int i=1;i<=n;i++)
for(int j=1;j<=m;j++)
scanf("%d",&w[i][j]);
for(int i=1;i<=n;i++)
for(int j=0;j<=m;j++)
for(int k=0;k<=j;k++)
f[i][j]=max(f[i][j],f[i-1][j-k]+w[i][k]);
printf("%d\n",f[n][m]);
int j=m;
for(int i=n;i;i--)
for(int k=0;k<=j;k++)
if(f[i][j]==f[i-1][j-k]+w[i][k])
{
ans[i]=k;
j-=k;
break;
}
for(int i=1;i<=n;i++)
printf("%d %d\n",i,ans[i]);
return 0;
}
AcWing 10
这个题目只是两两之间有依赖,不像分组背包。但是仔细观察发现,我们可以对某一个点的所有子树使用的容积分组,必须选该点,所以剩下的容积也要减去默认选择的自己,这个题就完美地转换成了上一道题目。在每一个子树内分别计算并决策每个子树分多少即可。注意,我们这里 f f f设定的是子树的容积,然而父节点在用我更新时用的是我的总容积,所以最后要加上自己的容积和贡献,且要把前面多出来(小于自己的容积)的部分清 0 0 0。
#include<bits/stdc++.h>
using namespace std;
const int NN=104;
int f[NN][NN],v[NN],w[NN],n,m;
vector<int>g[NN];
void dfs(int u)
{
for(int i=0;i<g[u].size();i++)
{
int son=g[u][i];
dfs(son);
for(int j=m-v[u];j>=0;j--)
for(int k=0;k<=j;k++)
f[u][j]=max(f[u][j],f[u][j-k]+f[son][k]);
}
for(int i=m;i>=v[u];i--)
f[u][i]=f[u][i-v[u]]+w[u];
for(int i=0;i<v[u];i++)
f[u][i]=0;
}
int main()
{
scanf("%d%d",&n,&m);
int root;
for(int i=1;i<=n;i++)
{
int fa;
scanf("%d%d%d",&v[i],&w[i],&fa);
if(fa==-1)
root=i;
else
g[fa].push_back(i);
}
dfs(root);
printf("%d",f[root][m]);
return 0;
}
背包求方案数
概念
如标题,背包问题改成了按题目要求求方案数。
例题
AcWing 278
这个题目要求装满的方案,则设 f i f_i fi为装满容量为 i i i的方案数。 f 0 = 1 f_0=1 f0=1其余等于 0 0 0,只有装满 0 0 0有一个什么都不装的方案。每一次有一个新物品,可以用它来装则又有了更多方案,则 f i + = f i − v f_i+=f_{i-v} fi+=fi−v。因为只有一个物品,所以要按 01 01 01背包的方式从大到小枚举。
#include<bits/stdc++.h>
using namespace std;
int f[10004];
int main()
{
int n,m;
scanf("%d%d",&n,&m);
f[0]=1;
for(int i=1;i<=n;i++)
{
int v;
scanf("%d",&v);
for(int j=m;j>=v;j--)
f[j]+=f[j-v];
}
printf("%d",f[m]);
return 0;
}
AcWing 11
这个题目要求在价值最大的前提下的方案数。设 f i f_i fi为恰好装 i i i的最大收益, g i g_i gi为恰好装 i i i且收益最大的方案数。 f f f的计算方法和以前的一样。 g g g首先只有 i = 0 i=0 i=0时有一种什么都不装的方案。如果 f f f是从 f i − v f_{i-v} fi−v转移的,则就可以用得到 f i − v f_{i-v} fi−v的方案( g i − v g_{i-v} gi−v)再选上该物品,方案数根据乘法原理, g i − v g_{i-v} gi−v乘上新加的方案(只有一种,用该物品),那么方案数加上 g i − v g_{i-v} gi−v的方案即可。如果 f f f没有变,那么加上原来的方案数即可,因为如果两个都恰好相等,则两种方案都能得到最大的结果,那么两种方案都是可行的,所以要加上而不是直接赋值。最后看所有的 f f f的最大值,并加上所有能得到最大收益的方案数。
#include<bits/stdc++.h>
using namespace std;
const int NN=1004,P=1e9+7;
int f[NN],g[NN];
int main()
{
int n,m;
scanf("%d%d",&n,&m);
memset(f,-0x3f,sizeof(f));
g[0]=1;
f[0]=0;
for(int i=1;i<=n;i++)
{
int v,w;
scanf("%d%d",&v,&w);
for(int j=m;j>=v;j--)
{
int maxx=max(f[j],f[j-v]+w),sum=0;
if(maxx==f[j])
sum=g[j];
if(maxx==f[j-v]+w)
sum=(g[j-v]+sum)%P;
f[j]=maxx;
g[j]=sum;
}
}
int maxx=0,ans=0;
for(int i=0;i<=m;i++)
if(f[i]>maxx)
{
ans=g[i];
maxx=f[i];
}
else if(f[i]==maxx)
ans=(ans+g[i])%P;
printf("%d",ans);
return 0;
}
背包求方案
概念
题目中会要求把方案输出。
方法
可以参考前面分组背包中第一题输出方案的方式,最后背包的总和是 m m m,每次看是从哪里迭代的,下标里少的就是用的容量,输出对应的是第几个物品即可。因为我们要看是从哪里迭代的,所以需要前面的东西参加计算,不能滚动数组。
例题
AcWing 12
这个题就是背包问题要求输出方案,按上述方法做即可。
#include<bits/stdc++.h>
using namespace std;
const int NN=1004;
int f[NN][NN],v[NN],w[NN];
int main()
{
int n,m;
scanf("%d%d",&n,&m);
for(int i=1;i<=n;i++)
scanf("%d%d",&v[i],&w[i]);
for(int i=n;i>=1;i--)
for(int j=0;j<=m;j++)
{
f[i][j]=f[i+1][j];
if(j>=v[i])
f[i][j]=max(f[i][j],f[i+1][j-v[i]]+w[i]);
}
int j=m;
for(int i=1;i<=n;i++)
if(j>=v[i]&&f[i][j]==f[i+1][j-v[i]]+w[i])
{
printf("%d ",i);
j-=v[i];
}
return 0;
}
习题
AcWing 1024
AcWing 1022
AcWing 1023
AcWing 1021
AcWing 1020
AcWing 426
AcWing 487
解析和代码在下一篇博客——状态机模型给出