动态规划dp
背包问题
01背包
n件物品,容量为V,第i件的费用空间Ci,价值是Wi,最大价值。
F[i, v] = MAX{F[i − 1, v], F[i − 1, v − Ci] + Wi}
void ZeroOnePack(ll cost,ll weight)
{
for(int i=v;i>=cost;i--)
f[i]=max_f(f[i],f[i-cost]+weight);
}
for(int i=1;i<=n;i++)
ZeroOnePack(c[i],w[i]);
printf("%lld\n",f[v]);
完全背包
N种物品,背包容量V,无限拿,i的空间为Ci,价值是Wi,总和最大。
F[i, v] = max{F[i − 1, v − kCi] + kWi | 0 ≤ kCi ≤ v}
void completepack(ll cost,ll weight)
{
for(int i=cost;i<=v;i++)
f[i]=max_f(f[i],f[i-cost]+weight);
}
for(int i=1;i<=n;i++)
completepack(c[i],w[i]);
printf("%lld\n",f[v]);
多重背包
N个物件,V的容量,最多只有Mi个,Ci,Wi,总和最大。
F[i,v] =max{F[i − 1, v − k ∗ Ci] + k ∗ Wi | 0 ≤ k ≤ Mi}
void zeroonepack(ll cost,ll weight)
{
for(int i=v;i>=cost;i--)
f[i]=max_f(f[i],f[i-cost]+weight);
}
void completepack(ll cost,ll weight)
{
for(int i=cost;i<=v;i++)
f[i]=max_f(f[i],f[i-cost]+weight);
}
void multiplepack(ll cost,ll weight,ll amount)
{
if(cost*amount>=v)
{
completepack(cost,weight);
return;
}
int k=1;
while(k<amount)
{
zeroonepack(k*cost,k*weight);
amount=amount-k;
k=k*2;
}
zeroonepack(amount*cost,amount*weight);
}
for(int i=1;i<=n;i++)
multiplepack(c[i],w[i],a[i]);
二维背包
对于每件物品,具有两种不同的费用;选择这件物品必须同时付出这两种代价;对于每种代价都有一个可付出的最大值(背包容量)。问怎样选择物品可以得到最大的价值。设这两种代价分别为代价1和代价2,第i物品所需的两种代价分别为w[i]和g[i].两种代价可付出的最大值(两种背包容量)分别为V和T。物品的价值为v[i].
f[i][v][u]=max(f[i−1][j][k],f[i−1][j−w[i]][k−g[i]]+v[i])
for (int i = 1; i <= n; i++)
for (int j = V; j >= a[i]; j--)
for (int k = T; k >= b[i]; k--)
f[j][k] = max(f[j][k], f[j - a[i]][k - b[i]] + w[i]);
分组背包问题
有N件物品和一个容量为V的背包。第i件物品的费用是c[i],价值是w[i]。这些物品被划分为若干组,每组中的物品互相冲突,最多选一件。求解将哪些物品装入背包可使这些物品的费用总和不超过背包容量,且价值总和最大。
这个问题变成了每组物品有若干种策略:是选择本组的某一件,还是一件都不选。也就是说设f[k][v]表示前k组物品花费费用v能取得的最大权值,则有:
f[k][v]=max{f[k-1][v],f[k-1][v-c[i]]+w[i]|物品i属于组k}
vector<int>k;
for (int t=1;t<k.size();t++)
for(int v=V;v>=0;v--)
for(int i:k[t])
f[v]=max{f[v],f[v-c[i]]+w[i]}
总结
以上涉及的各种背包问题都是要求在背包容量(费用)的限制下求可以取到的最大价值,但背包问题还有很多种灵活的问法,在这里值得提一下。但是我认为,只要深入理解了求背包问题最大价值的方法,即使问法变化了,也是不难想出算法的。
例如,求解最多可以放多少件物品或者最多可以装满多少背包的空间。这都可以根据具体问题利用前面的方程求出所有状态的值(f数组)之后得到。
还有,如果要求的是“总价值最小”“总件数最小”,只需简单的将上面的状态转移方程中的max改成min即可。
下面说一些变化更大的问法。
输出方案
以01背包为例,方程为f[i][v]=max{f[i-1][v],f[i-1][v-c[i]]+w[i]}。再用一个数组g[i][v],设g[i][v]=0表示推出f[i][v]的值时是采用了方程的前一项(也即f[i][v]=f[i-1][v]),g[i][v]表示采用了方程的后一项。注意这两项分别表示了两种策略:未选第i个物品及选了第i个物品。那么输出方案的伪代码可以这样写(设最终状态为f[N][V]):
i=N
v=V
while(i>0)
if(g[i][v]==0)
print "未选第i项物品"
else if(g[i][v]==1)
print "选了第i项物品"
v=v-c[i]
输出字典序最小的最优方案
按照前面经典的状态转移方程来求值,只是输出方案的时候要注意:从N到1输入时,如果f[i][v]==f[i-1][i-v]及f[i][v]==f[i-1][f-c[i]]+w[i]同时成立,应该按照后者(即选择了物品i)来输出方案。
求方案总数
这类改变问法的问题,一般只需将状态转移方程中的max改成sum即可。例如若每件物品均是完全背包中的物品,转移方程即为
f[i][v]=sum{f[i-1][v],f[i][v-c[i]]}
最优方案的总数
结合求最大总价值和方案总数两个问题的思路,最优方案的总数可以这样求:f[i][v]意义同前述,g[i][v]表示这个子问题的最优方案的总数,则在求f[i][v]的同时求g[i][v]的伪代码如下:
for i=1..N
for v=0..V
f[i][v]=max{f[i-1][v],f[i-1][v-c[i]]+w[i]}
g[i][v]=0
if(f[i][v]==f[i-1][v])
inc(g[i][v],g[i-1][v])
if(f[i][v]==f[i-1][v-c[i]]+w[i])
inc(g[i][v],g[i-1][v-c[i]])
钢管切割问题
现在给定一段n英寸长的钢条,求其切割方案,i英尺的价格为p[i]使得最终价格f[n]为最大
#include<iostream>
#include<cstdio>
#include<cstring>
using namespace std;
typedef long long ll;
int n;
ll f[1003],p[1003];
ll min_f(ll a,ll b)
{
if(a>b) return b;
else return a;
}
int main()
{
while(scanf("%d",&n)!=EOF)
{
ll q;
p[0]=0;
for(int i=1;i<=n;i++)
scanf("%lld",&p[i]);
f[0]=0;
for(int j=1;j<=n;j++)
{
q=1000004;
for(int i=1;i<=j;i++)
q=min_f(q,p[i]+f[j-i]);
f[j]=q;
}
printf("%lld\n",f[n]);
}
}
矩阵链相乘
给定n个矩阵{A1,A2,…,An},其中Ai与Ai+1是可乘的,i=1,2 ,…,n-1。如何确定计算矩阵连乘积的计算次序,使得依此次序计算矩阵连乘积需要的数乘次数最少。输入:有N个矩阵连乘,用一行有n+1个数数组表示,表示是n个矩阵的行及第n个矩阵的列,它们之间用空格隔开.
#include<cstdio>
#include<cstring>
#include<iostream>
#include<climits>
#define maxsize 303
using namespace std;
typedef long long ll;
int n;
ll m[maxsize][maxsize];
int s[maxsize][maxsize];
int p[maxsize];
void init()
{
for(int i=0;i<=n;i++)
scanf("%d",&p[i]);
memset(m,0,sizeof(m));
memset(s,0,sizeof(s));
}
void dp()
{
for(int i=1;i<=n;i++)
m[i][i]=0;
for(int l=2;l<=n;l++)
{
for(int i=1;i<=n-l+1;i++)
{
int j=i+l-1;
m[i][j]=INT_MAX;
for(int r=i;r<=j-1;r++)
{
ll q=m[i][r]+m[r+1][j]+p[i-1]*p[r]*p[j];
if(q<=m[i][j])
{
m[i][j]=q;
s[i][j]=max(r,s[i][j]);
}
}
}
}
}
void print_op(int i,int j)
{
if(i==j) printf("A%d",i);
else
{
printf("(");
print_op(i,s[i][j]);
print_op(s[i][j]+1,j);
printf(")");
}
}
void print()
{
printf("%d\n",m[1][n]);
print_op(1,n);
printf("\n");
}
int main()
{
while(~scanf("%d",&n))
{
init();
dp();
print();
}
}
最优二叉搜索树
给定一个n个不同关键字已排序的序列 K=<k1,k2,…,kn>(因此 k1<k2<…<kn ),我们希望用这些关键字构造一棵二叉搜索树。对每个关键字ki ,都有一个概率pi表示其搜索频率。有些要搜索的值可能不在 K中,因此我们还有n+1 个“伪关键字”d0,d1,d2…dn表示不在K中的值。d0 表示所有小于k1 的值dn 表示所有大于kn 的值,对 i=1,2,…,n−1i=1,2,…,n−1 i=1,2,…,n-1i=1,2,…,n−1 ,伪关键字di表示所有在ki 和ki+1 之间的值。对每个伪关键字di ,也都有一个概率 qi表示对应的搜索频率。
#include <iostream>
#include <cstdio>
#include <climits>
#define maxn 1010
using namespace std;
int e[maxn][maxn],w[maxn][maxn];
int root[maxn][maxn];
int n;
int p[maxn],q[maxn];
void scan(){
for(int i = 1;i <= n;i++){
scanf("%d", &p[i]);
}
for(int i = 0;i <= n;i++){
scanf("%d", &q[i]);
}
}
void init(){
for(int i = 0;i <= n + 1;i++){
for(int j = 0;j <= n + 1;j++){
e[i][j] = w[i][j] = root[i][j] = 0;
}
}
for(int i = 1;i <= n + 1;i++){
e[i][i - 1] = q[i - 1];
w[i][i - 1] = q[i - 1];
}
}
void DP(){
for(int l = 1;l <= n;l++){
for(int i = 1;i <= n - l + 1;i++){
int j = i + l - 1;
e[i][j] = INT_MAX;
w[i][j] = w[i][j - 1] + p[j] + q[j];
for(int r = i;r <= j;r++){
int t = e[i][r - 1] + e[r + 1][j] + w[i][j];
if(t < e[i][j]){
e[i][j] = t;
root[i][j] = r;
}
}
}
}
}
void print(){
printf("%d\n", e[1][n]);
}
int main()
{
while(~scanf("%d", &n)){
scan();
init();
DP();
print();
}
return 0;
}
最长公共子列
给定两个字符串,求解这两个字符串的最长公共子序列(Longest Common Sequence)。比如字符串1:BDCABA;字符串2:ABCBDAB
则这两个字符串的最长公共子序列长度为4,最长公共子序列是:BCBA
include<cstdio>
#include<cstring>
#include<iostream>
#define maxsize 10007
typedef long long ll;
ll max_f(ll a,ll b)
{
if(a>b) return a;
else return b;
}
ll x[maxsize],y[maxsize],dp[maxsize][maxsize];
int n,m;
int main()
{
while(scanf("%d%d",&n,&m)!=EOF)
{
for(int i=1;i<=n;i++)
scanf("%lld",&x[i]);
for(int i=1;i<=m;i++)
scanf("%lld",&y[i]);
memset(dp,0,sizeof(dp));
for(int i=1;i<=n;i++)
{
for(int j=1;j<=m;j++)
{
if(x[i]==y[j])
dp[i][j]=dp[i-1][j-1]+1;
else
dp[i][j]=max_f(dp[i][j-1],dp[i-1][j]);
}
}
printf("%lld\n",dp[n][m]);
}
}
区间dp
例如上面的矩阵链乘法和最优二叉搜索树,都是区间dp,进行转移
例题
题目描述
某一村庄在一条路线上安装了n盏路灯,每盏灯的功率有大有小(即同一段时间内消耗的电量有多有少)。老张就住在这条路中间某一路灯旁,他有一项工作就是每天早上天亮时一盏一盏地关掉这些路灯。
为了给村里节省电费,老张记录下了每盏路灯的位置和功率,他每次关灯时也都是尽快地去关,但是老张不知道怎样去关灯才能够最节省电。他每天都是在天亮时首先关掉自己所处位置的路灯,然后可以向左也可以向右去关灯。开始他以为先算一下左边路灯的总功率再算一下右边路灯的总功率,然后选择先关掉功率大的一边,再回过头来关掉另一边的路灯,而事实并非如此,因为在关的过程中适当地调头有可能会更省一些。
现在已知老张走的速度为1m/s,每个路灯的位置(是一个整数,即距路线起点的距离,单位:m)、功率(W),老张关灯所用的时间很短而可以忽略不计。
请你为老张编一程序来安排关灯的顺序,使从老张开始关灯时刻算起所有灯消耗电最少(灯关掉后便不再消耗电了)。
输入格式
文件第一行是两个数字n(1<=n<=50,表示路灯的总数)和c(1<=c<=n老张所处位置的路灯号);
接下来n行,每行两个数据,表示第1盏到第n盏路灯的位置和功率。数据保证路灯位置单调递增。
分析
区间两端点+大爷左/右这三要素为我们的状态,问题又接踵而至,如果我们现在知道了(l,r)(l,r)(l,r)这个子问题的最优解,我们怎么去得到更大问题的最优解?
我们考虑大爷一次关一盏灯,也就存在两种大情况(四种小情况):
对于l−1这一盏灯,大爷可以从l位置或者r位置跑过去关
对于r+1这一盏灯,大爷可以从l位置或者r位置跑过去关
#include<cstdio>
#include<iostream>
#include<cstring>
#include<queue>
#include<algorithm>
#include<cmath>
using namespace std;
const int maxn=52;
int n,c;
struct light
{
int c,p;
}l[maxn];
long long sum[maxn][maxn];
int dp[maxn][maxn][2];
long long ans;
int main()
{
scanf("%d%d",&n,&c);
for(int i=1;i<=n;i++)
{
scanf("%d%d",&l[i].c,&l[i].p);
}
for(int i=1;i<=n;i++)
{
sum[i][i]=l[i].p;
for(int j=i+1;j<=n;j++)
sum[i][j]=sum[i][j-1]+l[j].p;
}
memset(dp,0x3f,sizeof(dp));
if(c>=2) dp[c-1][c][0]=(l[c].c-l[c-1].c)*(sum[1][c-1]+sum[c+1][n]);
if(c<=n-1) dp[c][c+1][1]=(l[c+1].c-l[c].c)*(sum[1][c-1]+sum[c+1][n]);
for(int len=3;len<=n+1;len++)
{
for(int j=1;j<=n-len+2;j++)
{
int r=j+len-1;
dp[j][r][0]=min(dp[j+1][r][0]+(l[j+1].c-l[j].c)*(sum[1][j]+sum[r+1][n]),dp[j+1][r][1]+(l[r].c-l[j].c)*(sum[1][j]+sum[r+1][n]));
dp[j][r][1]=min(dp[j][r-1][0]+(l[r].c-l[j].c)*(sum[1][j-1]+sum[r][n]),dp[j][r-1][1]+(l[r].c-l[r-1].c)*(sum[1][j-1]+sum[r][n]));
}
}
printf("%lld",min(dp[1][n][0],dp[1][n][1]));
}
矩阵取数dp
注意好转移关系就好
例题
地面上出现了一个n*m的巨幅矩阵,矩阵的每个格子上有一坨0~k不等量的魔液。怪物各给了小a和uim一个魔瓶,说道,你们可以从矩阵的任一个格子开始,每次向右或向下走一步,从任一个格子结束。开始时小a用魔瓶吸收地面上的魔液,下一步由uim吸收,如此交替下去,并且要求最后一步必须由uim吸收。魔瓶只有k的容量,也就是说,如果装了k+1那么魔瓶会被清空成零,如果装了k+2就只剩下1,依次类推。怪物还说道,最后谁的魔瓶装的魔液多,谁就能活下来。小a和uim感情深厚,情同手足,怎能忍心让小伙伴离自己而去呢?沉默片刻,小a灵机一动,如果他俩的魔瓶中魔液一样多,不就都能活下来了吗?请输出能活下来的方法总数
分析
f[i][j][p][q]表示他们走到(i,j),且两人魔瓶内魔液量的差为p时的方法数。q=0表示最后一步是小a走的,q=1表示最后一步是uim走的。题目中说魔瓶的容量为k,实际上就是动归时p需要对k+1取余数,即p只有0~k,k+1种可能。答案为所有f[i][j][0][1]的和。
动归方程如下:(为了方便已经令k=k+1)
f[i][j][p][0]+=f[i-1][j][(p-mapp[i][j]+k)%k][1] (i-1>=1)
f[i][j][p][0]+=f[i][j-1][(p-mapp[i][j]+k)%k][1] (j-1>=1)
f[i][j][p][1]+=f[i-1][j][(p+mapp[i][j])%k][0] (i-1>=1)
f[i][j][p][1]+=f[i][j-1][(p+mapp[i][j])%k][0] (j-1>=1)
还有每个格子都有可能作为小a的起始格子,所以初始时对于所有i、j,f[i][j][mapp[i][j]][0]=1
#include<cstdio>
#include<cmath>
using namespace std;
int n,m,k,ans;
int f[805][805][20][2];
int a[805][805];
#define mod 1000000007
int main()
{
scanf("%d%d%d",&n,&m,&k);
k++;
for(int i=1;i<=n;i++)
{
for(int j=1;j<=m;j++)
{
scanf("%d",&a[i][j]);
f[i][j][a[i][j]][0]=1;
}
}
for(int i=1;i<=n;i++)
{
for(int j=1;j<=m;j++)
{
for(int h=0;h<=k;h++)
{
f[i][j][h][0]=(f[i][j][h][0]+f[i-1][j][(h-a[i][j]+k)%k][1])%mod;
f[i][j][h][0]=(f[i][j][h][0]+f[i][j-1][(h-a[i][j]+k)%k][1])%mod;
f[i][j][h][1]=(f[i][j][h][1]+f[i-1][j][(h+a[i][j])%k][0])%mod;
f[i][j][h][1]=(f[i][j][h][1]+f[i][j-1][(h+a[i][j])%k][0])%mod;
}
}
}
for(int i=1;i<=n;i++)
for(int j=1;j<=m;j++)
ans=(ans+f[i][j][0][1])%mod;
printf("%d",ans);
}
树状dp
树状dp有贪心和dp两种做法
例题
题目描述
2020年,人类在火星上建立了一个庞大的基地群,总共有n个基地。起初为了节约材料,人类只修建了n-1条道路来连接这些基地,并且每两个基地都能够通过道路到达,所以所有的基地形成了一个巨大的树状结构。如果基地A到基地B至少要经过d条道路的话,我们称基地A到基地B的距离为d。
由于火星上非常干燥,经常引发火灾,人类决定在火星上修建若干个消防局。消防局只能修建在基地里,每个消防局有能力扑灭与它距离不超过2的基地的火灾。
你的任务是计算至少要修建多少个消防局才能够确保火星上所有的基地在发生火灾时,消防队有能力及时扑灭火灾。
输入格式
输入文件的第一行为n (n<=1000),表示火星上基地的数目。接下来的n-1行每行有一个正整数,其中文件第i行的正整数为a[i],表示从编号为i的基地到编号为a[i]的基地之间有一条道路,为了更加简洁的描述树状结构的基地群,有a[i]<i。
贪心做法
其实贪心思想楼上都已经说的很清楚了,就是找最低没被覆盖到的点,并在它的祖父处设一个消防站。考虑到这个点的所有子孙后代都已经被覆盖了,因此这时覆盖祖父能盖到更多额外的点,并保证结果不会更差。
很多思路是用dfs或堆求取最低节点,实际上没必要,只要预处理出深度(边输入边处理)并排序,碰到已覆盖就跳过,未覆盖就在祖父处设消防站,ans++。
问题在于怎样才能判断这个点覆盖到了没有。对于儿子或孙子覆盖他,可以在在儿子处设站时就标记它;而对于父亲和祖父覆盖他,可以用儿子对父亲的映射f来解决;问题在于兄弟。其实,可以用o数组维护“离i最近的消防站到i的距离”,当o[父亲]==1时,就能确定它是否被覆盖。
#include<cstdio>
#include<iostream>
#include<algorithm>
using namespace std;
#define maxn 2010
int b[maxn],f[maxn],d[maxn],o[maxn],ans,u,v,w;
int n;
bool cmp(int x,int y)
{
return d[x]>d[y];
}
int main()
{
scanf("%d",&n);
b[1]=1,o[1]=o[0]=maxn;
for(int i=2;i<=n;i++)
{
scanf("%d",&f[i]);
d[i]=d[f[i]]+1,b[i]=i,o[i]=maxn;
}
sort(b+1,b+1+n,cmp);
for(int i=1;i<=n;i++)
{
v=b[i],w=f[v],u=f[f[v]];
o[v]=min(o[v],min(o[w]+1,o[u]+2));
if(o[v]>2)
{
o[u]=0;ans++;
o[f[u]]=min(o[f[u]],1),o[f[f[u]]]=min(o[f[f[u]]],2);
}
}
printf("%d",ans);
}
dp做法
F[i][0]表示可以覆盖到从节点i向上2层的最小消防站个数
F[i][1]表示可以覆盖到从节点i向上1层的最小消防站个数
F[i][2]表示可以覆盖到从节点i向上0层的最小消防站个数
F[i][3]表示可以覆盖到从节点i向上-1层的最小消防站个数
F[i][4]表示可以覆盖到从节点i向上-2层的最小消防站个树
“覆盖到某层”的意思是在这棵子树中这一层和其以下层的所有点都被消防站覆盖到。
#include<cstdio>
#include<iostream>
#include<climits>
using namespace std;
const int maxn=1010;
int n;
struct edge
{
int to;
int next;
}sons[maxn];
int head[maxn]={0};//前向星存图
int f[maxn][5];
int nowedge=0;
void addson(int u,int v)
{
nowedge++;
sons[nowedge].to=v;
sons[nowedge].next=head[u];
head[u]=nowedge;
}
void dfs(int t)
{
f[t][0]=1;
f[t][3]=0;
f[t][4]=0;
for(int i=head[t];i;i=sons[i].next)
{
int s=sons[i].to;
dfs(s);
f[t][0]+=f[s][4];
f[t][3]+=f[s][2];
f[t][4]+=f[s][3];
}
if(head[t]==0)
{
f[t][1]=f[t][2]=1;
}
else
{
f[t][1]=f[t][2]=INT_MAX;
for(int i=head[t];i;i=sons[i].next)
{
int s=sons[i].to;
int f1=f[s][0];
int f2=f[s][1];
for(int j=head[t];j;j=sons[j].next)
{
if(i==j) continue;
int a=sons[j].to;
f1+=f[a][3];
f2+=f[a][2];
}
f[t][1]=min(f[t][1],f1);
f[t][2]=min(f[t][2],f2);
}
}
for(int i=1;i<=4;i++)
{
f[t][i]=min(f[t][i],f[t][i-1]);
}
}
int main()
{
scanf("%d",&n);
for(int i=2;i<=n;i++)
{
int f;
scanf("%d",&f);
addson(f,i);
}
dfs(1);
printf("%d",f[1][2]);
}