ACM Weekly 10(待修改)

前言

本文主要参考《背包九讲》与背包九讲——全篇详细理解与代码实现,阅读之后,茅塞顿开,鄙人只能微笑、默叹、以为妙绝,如有涉及侵权,鄙人一定修改或删除

涉及的知识点

四个字:动态规划

01背包

基本理论

01背包的基本应用场景为:有一个容量V的背包,有N个各自价值为 v i v_i vi,体积各为 w i w_i wi的物品,采用怎样的放置物品的顺序才能达到总价值最大?

对于每一件物品来说,有放与不放两种选择,定义 d p [ i ] [ j ] dp[i][j] dp[i][j]前i个物品放在容量为j的背包中可获得的最大价值,对于 d p [ i ] [ j ] dp[i][j] dp[i][j],它有两个来源,一个为不选第i个物品,则值与 d p [ i − 1 ] [ j ] dp[i-1][j] dp[i1][j](前i-1个物品放在容量为j的背包中可获得的最大值)相同,一个为选第i个物品,则值与 d p [ i − 1 ] [ j − w [ i ] ] + v [ i ] dp[i-1][j-w[i]]+v[i] dp[i1][jw[i]]+v[i](前i-1个物品放在容量为j-w[i]的背包中获得的最大)

代码

for(int i=0;i<n;i++)
	for(int j=0;j<=V;j++)
		if(j<w[i])
			dp[i][j]=dp[i-1][j];
		else
			dp[i][j]=max(dp[i-1][j],dp[i-1][j-w[i]]+v[i]);

优化

时间复杂度上已经不能再进行什么优化,空间复杂度可以进行进一步的优化,观察二重循环可知,对于当前的第i层,我们所需要的先前状态都属于i-1层,也就是说,我们只需当前层只需要上一层的信息即可,那么便可以只用一个一维数组来进行动态规划,状态转移为 d p [ j ] = m a x ( d p [ j ] , d p [ j − w [ i ] ] + v [ i ] ) dp[j]=max(dp[j],dp[j-w[i]]+v[i]) dp[j]=max(dp[j],dp[jw[i]]+v[i])将上一层的数据实时更改即可,那么,循环的顺序是否不变呢?

由推论可知,只是用一个一维数组的前提条件是前i-1层的状态已经全部求出,且推第i层的任意项时,确保dp[j]与dp[j-w[i]]项仍然保存的是第i-1层的数据,那么现在再来看看循环次序,首先,主循环定为从0 ~n-1,代表第几层,那么j的循环次序是V ~0还是0 ~V呢?

假设为后者,那么求值时按照j从小到大来求值,当到达j时,它需要 d p [ j ] dp[j] dp[j] d p [ j − w [ i ] ] + v [ i ] dp[j-w[i]]+v[i] dp[jw[i]]+v[i],此时,由于 d p [ j ] dp[j] dp[j]还没有更新,所以它等效于 d p [ i − 1 ] [ j ] dp[i-1][j] dp[i1][j],但对于 d p [ j − w [ i ] ] + v [ i ] dp[j-w[i]]+v[i] dp[jw[i]]+v[i],当循环到j时, d p [ j − w [ i ] ] dp[j-w[i]] dp[jw[i]]必然已经遍历到了,此时的 d p [ j − w [ i ] ] + v [ i ] dp[j-w[i]]+v[i] dp[jw[i]]+v[i]等效于 d p [ i ] [ j − w [ i ] ] + v [ i ] dp[i][j-w[i]]+v[i] dp[i][jw[i]]+v[i],并不满足能使用一维数组的前提条件,所以前者成立

那么我们便可以得出代码

代码

for(int i=0;i<n;i++)
	for(int j=V;j>=0;j--)
		if(j>=w[i])
			dp[j]=max(dp[j],dp[j-w[i]]+v[i]);

上面的代码依然可以简化,通过if的判断我们可以知道,只有当j>=w[i]即装得下的时候,才会更新dp[j],那么我们只需要考虑V~w[i]的值即可

代码

for(int i=0;i<n;i++)
	for(int j=V;j>=w[i];j--)
			dp[j]=max(dp[j],dp[j-w[i]]+v[i]);

对于 d p [ j ] dp[j] dp[j]来说,我们只需要知道 d p [ j − w [ i ] ] dp[j-w[i]] dp[jw[i]]即可,因为 d p [ j ] dp[j] dp[j]本身就是 d p [ i − 1 ] [ j ] dp[i-1][j] dp[i1][j](未更新时),对于一维数组,我们最后只需要知道 d p [ j ] dp[j] dp[j]的值即可,而由前一句话,我们知道 d p [ j ] dp[j] dp[j]只需要 d p [ j − w [ n − 1 ] ] dp[j-w[n-1]] dp[jw[n1]](放入第n个),要知道 d p [ j − w [ n ] ] dp[j-w[n]] dp[jw[n]]只需要 d p [ j − w [ n − 1 ] − w [ n − 2 ] ] dp[j-w[n-1]-w[n-2]] dp[jw[n1]w[n2]],以此类推,对于 d p [ j ] dp[j] dp[j],我们只需 d p [ j − s u m ( i + 1 , n − 1 ) ] dp[j-sum(i+1,n-1)] dp[jsum(i+1,n1)],使用前缀和后,代码如下

代码

for(int i=0;i<n;i++)
{
	cin >>w[i]>>v[i];
	pre[i]=pre[i-1]+w[i];
}
for(int i=0;i<n;i++)
{
	int t=max(w[i],V-(pre[n-1]-pre[i]));
	for(int j=V;j>=t;j--)
		dp[j]=max(dp[j],dp[j-w[i]]+v[i]);//去掉的是w[i],不是t
}

关于初始化

对于01背包问题,题目的要求有时需要装满,有时只需要价值最大,前者初始化时dp[0]为0,其余为 − ∞ -\infty ,后者全初始化为0。可以这样理解,dp中的每个值为背包与物品存放的一种状态,在第一种情况,只用容量为0价值为0为可以存在的状态,其他都是未装满的,属于未定义状态,后者则无此约束,因为各容量为0允许存在

例题

HDU 2602

题目大意:背包容量为V,N个骨头,各自有各自价格与体积,求最大价值

思路:01背包模板

代码

#include <iostream>
#include <cstring>
using namespace std;
int T,N,V,dp[1212],value[1212],weight[1212];
int main()
{
    //freopen("test.txt","r",stdin);
    cin >>T;
    while(T--)
    {
        cin >>N>>V;
        for(int i=1; i<=N; i++)
            cin >>value[i];
        for(int i=1; i<=N; i++)
            cin >>weight[i];
        for(int i=1; i<=N; i++)
            for(int j=V; j>=0; j--)
                if(j>=weight[i])
                    dp[j]=max(dp[j],dp[j-weight[i]]+value[i]);
        cout <<dp[V]<<endl;
        memset(dp,0,sizeof(dp));
        memset(value,0,sizeof(value));
        memset(weight,0,sizeof(weight));
    }
    return 0;
}

Luogu p2925

题目大意:略

思路:01背包模板

代码

#include <iostream>
#include <cstdlib>
#include <cstdio>
using namespace std;
int C,H,V[12121],dp[121212],ans;
int main()
{
    scanf("%d%d",&C,&H);
    for(int i=0; i<H; i++)
        scanf("%d",&V[i]);
    for(int i=0; i<H; i++)
        for(int j=C; j>=V[i]; j--)
        {
            dp[j]=max(dp[j],dp[j-V[i]]+V[i]);
            ans=max(dp[j],ans);
        }
    printf("%d",ans);
    return 0;
}

HDU 3466

题目大意:给出N个物品,钱为M,每个物品对应了三个值, P i P_i Pi为花费, Q i Q_i Qi为所拥有钱至少为该值时才能放入, V i V_i Vi为价值, P i ≤ Q i P_i \le Q_i PiQi 恒成立,求最大价值

思路:一般的01背包问题是直接顺序的,但是该题则要求另一种根据Q的顺序,同样的价钱进行不同的购买顺序可能会因Q的限制而产生不同的收益,对于同样的两件物品A、B,对应的值为 P i 、 Q i 、 P j 、 Q j P_i、Q_i、P_j、Q_j PiQiPjQj,假设所需钱能满足两者花费和,如果购买顺序为A->B,那么就需要拥有至少 P i + Q j P_i+Q_j Pi+Qj的钱,反之需要 P j + Q i P_j+Q_i Pj+Qi,我们需要考虑其中的较小值,如果执行前者更小,可得 Q i − P i < Q j − P j Q_i-P_i<Q_j-P_j QiPi<QjPj的不等式,满足该不等式时,为A->B,反之为B->A,也就是我们优先选择期望拥有钱数与实际需要价格之间差值较小者,那么按照这条原则进行排序,再按照01背包的模板即可

代码

#include <iostream>
#include <cstring>
#include <cstdlib>
#include <algorithm>
#include <cstdio>
using namespace std;
typedef struct Item
{
    int p,q,v;
    bool operator<(Item x)const
    {
        return q-p<x.q-x.p;
    }
}Item;
Item goods[520];
int dp[12121],N,M;
int main()
{
    while(scanf("%d%d",&N,&M)!=EOF)
    {
        for(int i=0;i<N;i++)
            scanf("%d%d%d",&goods[i].p,&goods[i].q,&goods[i].v);
        sort(goods,goods+N);
        for(int i=0;i<N;i++)
            for(int j=M;j>=goods[i].q;j--)
                dp[j]=max(dp[j],dp[j-goods[i].p]+goods[i].v);
        printf("%d\n",dp[M]);
        memset(dp,0,sizeof(dp));
    }
    return 0;
}

完全背包

基本理论

完全背包的基本应用场景为:有一个容量V的背包,有N种各自价值为 v i v_i vi,体积各为 w i w_i wi的物品,每种物品可选无限次,采用怎样的放置物品的顺序与数量才能达到总价值最大?

相对于01背包,完全背包采用的策略从该物品拿还是不拿,变成了拿多少个

那么根据01背包的模板,我们可以延伸出完全背包的初级模板,对每一种物品进行能放入的状态假设,可以得到这样的状态转移方程: d p [ j ] = m a x ( d p [ j ] , d p [ j − k × w [ j ] ] + k × v [ j ] ] ) ( 0 ≤ j − k × w [ j ] ≤ V ) dp[j]=max(dp[j],dp[j-k×w[j]]+k×v[j]])(0\le j-k×w[j]\le V ) dp[j]=max(dp[j],dp[jk×w[j]]+k×v[j]])(0jk×w[j]V)

优化

上述方法的时间复杂度为 O ( N ∑ V w [ i ] ) O(N\sum \frac{V}{w[i]}) O(Nw[i]V),显然较为复杂,首先有个较为简单的优化:对于两件物品i、j,如果i的价值大于j,i的花费小于j,则去掉j物品,因为在同等情况下,取i的所带来的收益肯定大于j,总体来看这样的优化至少不会得到收益更差的方案,然而对于被设计后的数据,这种方法无能为力,可能无法去除物品,在此基础上可以进一步优化,首先去掉大于花费大于V的物品,然后利用桶排序的原理记下花费相同的物品中价值最高者,时间复杂度为 O ( V + N ) O(V+N) O(V+N),代码如下

代码

unordered_map<int,int>best;
for(int i=1; i<=n; i++)
{
    int w,v;
    scanf("%d%d",&w,&v);
    if(w>V)
        continue;
    if(v>best[w])
        best[w]=v;
}

完全背包问题可以直接转换为01背包问题来解决,即将第i种物品划分成取0~k份单独物品所构成的整体,求解这样的01背包问题,当然时间复杂度没有改变

更好的以01背包思路来解决的方法为:将第i件物品拆分为 w [ i ] × 2 k w[i]×2^k w[i]×2k,价值为 v [ i ] × 2 k v[i]×2^k v[i]×2k的物品,k满足 w [ i ] × 2 k ≤ V w[i]×2^k\le V w[i]×2kV,不管选多少件i种物品,总可以将个数表示为二进制数,这样的时间复杂度变成了 O ( N ∑ log ⁡ ( V w [ i ] ) ) O(N\sum \log (\frac{V}{w[i]})) O(Nlog(w[i]V))

更高效的简化,是直接利用01背包的代码,代码如下

代码

for(int i=0;i<n;i++)
	for(int j=w[i];j<=V;j++)
		dp[j]=max(dp[j],dp[j-w[i]]+v[i]);

解释:相对于01背包的代码,完全背包的代码只是将第二层循环的j的顺序进行了颠倒,为什么这样就能实现?首先明确一个区别,01背包和完全背包的不同之处在于01只取一次,完全可取多次,01的逆序枚举是为了确保对当前 d p [ j ] dp[j] dp[j]所涉及到的先前状态未改变,保证上一物品已经取完一次,即确保每个物品只取一次,对于顺序存储来说, d p [ j ] dp[j] dp[j]等价于 d p [ i − 1 ] [ j ] dp[i-1][j] dp[i1][j],但是 d p [ j − w [ i ] ] dp[j-w[i]] dp[jw[i]]等价于 d p [ i ] [ j − w [ i ] ] dp[i][j-w[i]] dp[i][jw[i]],因为当循环到达j时, d p [ j − w [ i ] ] dp[j-w[i]] dp[jw[i]]存储的已经不是第i-1时候的值了,它已经被更新成了到达第i个物品的值, 即 d p [ i ] [ j − w [ i ] ] dp[i][j-w[i]] dp[i][jw[i]],对于完全背包来说,它的先前状态为该物品未装进与该物品已经装了k个判断k+1个是否能装,那么 d p [ j − w [ i ] ] dp[j-w[i]] dp[jw[i]]正好符合后者,因为它为相对于自身的上一状态又追加了一个物品的结果,最后可以得到完全背包的状态转移方程: d p [ j ] = m a x ( d p [ j ] , d p [ j − w [ i ] ] + v [ i ] ) dp[j]=max(dp[j],dp[j-w[i]]+v[i]) dp[j]=max(dp[j],dp[jw[i]]+v[i])

例题

HDU 1114

题目大意:给出T个样例,每个样例背包容量为F-E,N种物品,每种物品价值为 P i P_i Pi,重量为 W i W_i Wi,求出总价值最少且能装满背包的最优解

思路:在完全背包的基础上更改判断规则,最大值改成最小值,初始化时将除0外的值更改为无穷大

代码

#include <iostream>
#include <algorithm>
#include <cstring>
#include <cstdlib>
#include <cstdio>
using namespace std;
int T,E,F,N,dp[12121],w[12121],p[12121];
int main()
{
    scanf("%d",&T);
    while(T--)
    {
        scanf("%d%d%d",&E,&F,&N);
        for(int i=0; i<N; i++)
            scanf("%d%d",&p[i],&w[i]);
        for(int i=1; i<=F-E; i++)
            dp[i]=9999999;
        dp[0]=0;
        for(int i=0; i<N; i++)
            for(int j=w[i]; j<=F-E; j++)
                dp[j]=min(dp[j],dp[j-w[i]]+p[i]);
        if(dp[F-E]!=9999999)
            printf("The minimum amount of money in the piggy-bank is %d.\n",dp[F-E]);
        else
            printf("This is impossible.\n");
    }
    return 0;
}

Luogu P1853

题目大意:略

思路:完全背包模板,每一年更新背包容量(资产)即可

代码

#include <iostream>
#include <cstdlib>
#include <cstdio>
using namespace std;
typedef long long ll;
ll S,N,D,dp[100000000],w[1212],p[1212];
int main()
{
    scanf("%lld%lld%lld",&S,&N,&D);
    for(ll i=1;i<=D;i++)
        scanf("%lld%lld",&w[i],&p[i]);
    while(N--)
    {
       for(ll i=1;i<=D;i++)
        for(ll j=w[i];j<=S;j++)
            dp[j]=max(dp[j],dp[j-w[i]]+p[i]);
        S+=dp[S];
    }
    printf("%lld",S);
    return 0;
}

多重背包

基本理论

多重背包的基本应用场景为:有一个容量V的背包,有N种各自价值为 v i v_i vi,体积各为 w i w_i wi的物品,每种物品有 X i X_i Xi个,采用怎样的放置物品的顺序与数量才能达到总价值最大?

多重背包的基本状态转移方程和完全背包类似,只是将物品所取的数量改成了物品数与容量除以单个体积,即 d p [ i ] [ j ] = m a x ( d p [ i − 1 ] [ j − k ∗ w [ i ] ] + k ∗ v [ i ] ) ( 0 ≤ k ≤ x [ i ] ) dp[i][j]=max(dp[i-1][j-k*w[i]]+k*v[i])(0\le k\le x[i]) dp[i][j]=max(dp[i1][jkw[i]]+kv[i])(0kx[i]),复杂度为 O ( N × ∑ x [ i ] ) O(N×\sum x[i]) O(N×x[i])

当然也能和完全背包一样直接转换成01背包来实现,将取0~x[i]件物品的情况单独分为不同的物体,复杂度不变

优化

更好的优化是采用先前提到的二进制的方法,将第i种物品分成拥有若干个系数的物品,为确保0~x[i]内的每一个值能被系数和表示,系数序列为: 1 、 2 、 4 、 … 2 k − 1 、 x [ i ] − 2 k + 1 1、2、4、\dots 2^{k-1}、x[i]-2^k+1 1242k1x[i]2k+1,这样时间复杂度便成为了 O ( N × ∑ log ⁡ x [ i ] ) O(N×\sum \log x[i]) O(N×logx[i]),代码实现如下

代码

for (int i = 1; i <= n; i++) {
    int num = min(x[i], V / w[i]);
    for (int k = 1; num > 0; k <<= 1) {
        if (k > num) k = num;
        num -= k;
        for (int j = V; j >= w[i] * k; j--)
            dp[j] = max(dp[j], dp[j - w[i] * k] + v[i] * k);
    }
}

单调队列&优化

例题

HDU 1059

题目大意:给出6种物品,价值为1~6,给出各自的个数,求总价值能否平分为两份

思路:背包容量为总价值的一半,以价值/容量为所求期望,判断最后能否装满即可

代码

#include <iostream>
#include <cstring>
#include <cstdlib>
#include <cstdio>
using namespace std;
int x[7],dp[212121],ans,cnt;
bool flag=true;
int main()
{
    while(flag)
    {
        flag=false;
        ans=0;
        for(int i=1; i<=6; i++)
        {
            scanf("%d",&x[i]);
            ans+=x[i]*i;
            flag|=x[i];
        }
        if(!flag)
            break;
        printf("Collection #%d:\n",++cnt);
        if(ans%2!=0)
        {
            printf("Can't be divided.\n\n");
            continue;
        }
        ans>>=1;
        memset(dp,0,sizeof(dp));
        for(int i=1; i<=6; i++)
        {
            int num=min(x[i],ans/i);
            for(int k=1; num>0; k<<=1)
            {
                if(k>num)k=num;
                num-=k;
                for(int j=ans; j>=k*i; j--)
                    dp[j]=max(dp[j],dp[j-k*i]+k*i);
            }
        }
        if(dp[ans]==ans)
            printf("Can be divided.\n\n");
        else
            printf("Can't be divided.\n\n");
    }
    return 0;
}

Luogu P1776

题目大意:略

思路:多重背包模板

代码

#include <iostream>
#include <cstdlib>
#include <cstdio>
using namespace std;
int N,V,v[121212],w[121212],m[121212],dp[121212];
int main()
{
    scanf("%d%d",&N,&V);
    for(int i=1; i<=N; i++)
        scanf("%d%d%d",&v[i],&w[i],&m[i]);
    for(int i=1; i<=N; i++)
    {
        int num=min(m[i],V/w[i]);
        for(int k=1; num>0; k<<=1)
        {
            if(k>num)k=num;
            num-=k;
            for(int j=V; j>=k*w[i]; j--)
                dp[j]=max(dp[j],dp[j-k*w[i]]+v[i]*k);
        }
    }
    printf("%d",dp[V]);
    return 0;
}

混合背包

基本理论

混合背包的基本应用场景为:有一个容量V的背包,有N种各自价值为 v i v_i vi,体积各为 w i w_i wi的物品,每种物品有1个或 X i X_i Xi个或无穷个,采用怎样的放置物品的顺序与数量才能达到总价值最大?

混合背包=01背包+完全背包+多重背包

如果只有01背包和完全背包,从两者的代码我们可以看到,两者的区别是在第二层循环的顺序与逆序上,而对于01背包和完全背包共存的情形,是不存在01背包处理过该物品后,完全背包还会处理该物品的情况的,反之亦然,也就是说无论是先01后完全还是相反顺序,它们都可以在上一层的基础上进行当前状态的推导,代码如下:

代码

for(int i=1;i<=n;i++)
{
	if(x[i]==0)//假设为无穷大的标记
		for(int j=w[i];j<=V;j++)
			dp[j]=max(dp[j],dp[j-w[i]]+v[i]);
	else
		for(int j=V;j>=w[i];j++)
			dp[j]=max(dp[j],dp[j-w[i]]+v[i]);
}

如果再加上多重背包,可以将其与01背包结合,视01背包为多重背包的特殊情形,更简单的方法是根据物品数量直接调用相关的背包模型,代码如下

代码(视01背包为多重背包特殊)

for(int i=1;i<=n;i++)
{
	if(x[i]==0)//假设为无穷大的标记
		for(int j=w[i];j<=V;j++)
			dp[j]=max(dp[j],dp[j-w[i]]+v[i]);
	else
		for(int k=1;k<=x[i];k++)
			for(int j=V;j>=w[i];j++)
				dp[j]=max(dp[j],dp[j-w[i]]+v[i]);
}

代码(根据背包模型调用)

for(int i=1;i<=n;i++)
{
	if(x[i]==0)//完全
		for(int j=w[i];j<=V;j++)
			dp[j]=max(dp[j],dp[j-w[i]]+v[i]);
	else if(x[i]==1)//01
		for(int j=V;j>=w[i];j++)
			dp[j]=max(dp[j],dp[j-w[i]]+v[i]);
	else//多重
		{
			int num=min(x[i],V/w[i]);
			for(int k=1;num>0;k<<=1)
			{
				if(k>num)k=num;
				num-=k;
				for(int j=V;j>=w[i]*k;j--)
					dp[j]=min(dp[j],dp[j-w[i]*k]+v[i]*k);
			}
		}
}

例题

Luogu P1833

题目大意:略

思路:混合背包模板

代码

#include <iostream>
#include <cstdlib>
#include <cstdio>
using namespace std;
int Tsh,Tsm,Teh,Tem,N,T,w[12121],v[12121],p[12121],dp[121212];
int main()
{
    scanf("%d:%d%d:%d%d",&Tsh,&Tsm,&Teh,&Tem,&N);
    T=(Teh-Tsh)*60+Tem-Tsm;
    for(int i=1; i<=N; i++)
        scanf("%d%d%d",&w[i],&v[i],&p[i]);
    for(int i=1; i<=N; i++)
    {
        if(p[i]==0)
            for(int j=w[i]; j<=T; j++)
                dp[j]=max(dp[j],dp[j-w[i]]+v[i]);
        else if(p[i]==1)
            for(int j=T; j>=w[i]; j--)
                dp[j]=max(dp[j],dp[j-w[i]]+v[i]);
        else
        {
            int num=min(p[i],T/w[i]);
            for(int k=1; num>0; k<<=1)
            {
                if(k>num)k=num;
                num-=k;
                for(int j=T; j>=w[i]*k; j--)
                    dp[j]=max(dp[j],dp[j-w[i]*k]+v[i]*k);
            }
        }
    }
    printf("%d",dp[T]);
    return 0;
}

HDU 3535

题目大意:给出多组物品,每组物品有多项,并且各属于以下三种类别:0,该组至少选一件,1,该组至多选一件,2,该组可选任意数量,给出各物品的体积与价值,给出背包体积,求最大值

思路:对于每一种情况进行考虑:0,至少选一件,则代表必须要选,对应着“必须装满”的概念,将值初始化为负无穷,均设置为非法状态,状态转移方程为 d p [ i ] [ k ] = m a x ( d p [ i ] [ k ] , d p [ i − 1 ] [ k − w [ i ] ] + v [ i ] , d p [ i ] [ k − w [ i ] ] + v [ i ] ) dp[i][k]=max(dp[i][k],dp[i-1][k-w[i]]+v[i],dp[i][k-w[i]]+v[i]) dp[i][k]=max(dp[i][k],dp[i1][kw[i]]+v[i],dp[i][kw[i]]+v[i]),含义分别为不选该物品,首次选该物品(使用上一物品的结果),非首次选该物品(i不变);1,最多选一件,类似01背包,需要先复制上一物品的结果作为参照;2,任选,类似完全背包,状态方程同0

代码

#include <iostream>
#include <cstdlib>
#include <cstring>
#include <cstdio>
using namespace std;
int n,T,w[1212],v[1212],dp[1212][1212];
int main()
{
    while(scanf("%d%d",&n,&T)!=EOF)
    {
        memset(dp,0,sizeof(dp));//清空
        for(int i=1; i<=n; i++)//以组为单位进行规划
        {
            int m,s;
            scanf("%d%d",&m,&s);
            for(int j=1; j<=m; j++)
                scanf("%d%d",&w[j],&v[j]);
            switch(s)
            {
            case 0://如果为0,代表必须选一件,初始化为非法值
                for(int j=0; j<=T; j++)
                    dp[i][j]=-INT_MAX;
                for(int j=1; j<=m; j++)
                    for(int k=T; k>=w[j]; k--)//以下为不选,第一次选,非第一次选
                        dp[i][k]=max(dp[i][k],max(dp[i-1][k-w[j]]+v[j],dp[i][k-w[j]]+v[j]));
                break;
            case 1:
                for(int j=0; j<=T; j++)//获得上一组完成后的值
                    dp[i][j]=dp[i-1][j];
                for(int j=1; j<=m; j++)
                    for(int k=T; k>=w[j]; k--)//类似01
                        dp[i][k]=max(dp[i][k],dp[i-1][k-w[j]]+v[j]);
                break;
            default:
                for(int j=0; j<=T; j++)//获得上一组完成后的值
                    dp[i][j]=dp[i-1][j];
                for(int j=1; j<=m; j++)
                    for(int k=T; k>=w[j]; k--)//不选,第一次选,非第一次选
                        dp[i][k]=max(dp[i][k],max(dp[i-1][k-w[j]]+v[j],dp[i][k-w[j]]+v[j]));
                break;
            }
        }
        printf("%d\n",max(dp[n][T],-1));
    }
    return 0;
}

二维费用背包

基本理论

二维费用背包的基本应用场景为:有一个容量 V V V,称重为 W W W的背包,有N种各自价值为 v i v_i vi,体积各为 w i w_i wi,重量各为 s i s_i si的物品,每种物品有1个或 X i X_i Xi个或无穷个,采用怎样的放置物品的顺序与数量才能达到总价值最大?

通俗来说,二维费用背包其实就是给背包多加了一层限制条件,有些类似于高中数学的二维线性规划,费用的约束增加了一维,状态增加一维即可,如果设 d p [ i ] [ j ] [ k ] dp[i][j][k] dp[i][j][k]为前i件物品在容量为j,承重为k的背包中的最大值,可得状态转移方程为: d p [ i ] [ j ] [ k ] = m a x ( d p [ i − 1 ] [ j ] [ k ] , d p [ i − 1 ] [ j − w [ i ] ] [ k − s [ i ] ] + v [ i ] ) dp[i][j][k]=max(dp[i-1][j][k],dp[i-1][j-w[i]][k-s[i]]+v[i]) dp[i][j][k]=max(dp[i1][j][k],dp[i1][jw[i]][ks[i]]+v[i]),代码如下

代码

for(int i=1;i<=n;i++)
{
	if(x[i]==0)
		for(int j=w[i];j<=V;j++)
			for(int k=s[i];k<=W;k++)
				dp[j][k]=max(dp[j][k],dp[j-w[i]][k-s[i]]+v[i]);
	else if(x[i]==1)
		for(int j=V;j>=w[i];j--)
			for(int k=W;k>=s[i];k--)
				dp[j][k]=max(dp[j][k],dp[j-w[i]][k-s[i]]+v[i]);
	else
	{
		int num=min(x[i],V/w[i],W/s[i]);
		for(int k=1;num>0;k<<=1)
		{
			if(k>num)k=num;
			num-=k;
			for(int j=V;j>=w[i]*k;j--)
				for(int t=W;t>=s[i]*k;t--)
					dp[j][t]=max(dp[j][t],dp[j-w[i]*k][t-s[i]*k]+v[i]*k);
		}
	}
}

例题

Luogu 1507

题目大意:略

思路:二维费用背包模板+01背包

代码

#include <iostream>
#include <cstdlib>
#include <cstdio>
using namespace std;
int V,M,N,w[121],m[121],v[121],dp[1212][1212];
int main()
{
    scanf("%d%d%d",&V,&M,&N);
    for(int i=1; i<=N; i++)
        scanf("%d%d%d",&w[i],&m[i],&v[i]);
    for(int i=1; i<=N; i++)
        for(int j=V; j>=w[i]; j--)
            for(int k=M; k>=m[i]; k--)
                dp[j][k]=max(dp[j][k],dp[j-w[i]][k-m[i]]+v[i]);
    printf("%d",dp[V][M]);
    return 0;
}

HDU 2159

题目大意:略

思路:二维费用背包模板+完全背包

代码

#include <iostream>
#include <cstdio>
#include <cstdlib>
#include <cstring>
using namespace std;
int N,M,K,S;
int dp[1212][1212],pay[1212],value[1212];
int main()
{
    while(scanf("%d%d%d%d",&N,&M,&K,&S)!=EOF)
    {
        for(int i=1; i<=K; i++)
            scanf("%d%d",&value[i],&pay[i]);
        for(int i=1; i<=K; i++)
            for(int j=pay[i]; j<=M; j++)
                for(int t=1; t<=S; t++)
                    dp[j][t]=max(dp[j][t],dp[j-pay[i]][t-1]+value[i]);
        if(dp[M][S]>=N)
        {
            for(int i=0; i<=M; i++)
                if(dp[i][S]>=N)
                {
                    printf("%d\n",M-i);
                    break;
                }
        }
        else
            printf("-1\n");
        memset(dp,0,sizeof(dp));
        memset(pay,0,sizeof(pay));
        memset(value,0,sizeof(value));
    }
    return 0;
}

分组背包

基本理论

分组背包的基本应用场景为:有一个容量V的背包,有N种各自价值为 v i v_i vi,体积各为 w i w_i wi的物品,这些物品被划分成若干组,每组物品相互冲突,最多选一件,采用怎样的放置物品的顺序与数量才能达到总价值最大?

分组背包使得取舍的对象宏观上来看由个体抽象到了组,其实是有限的状态遍历,假设 d p [ k ] [ j ] dp[k][j] dp[k][j]表示前k组物品容量为j的最大值,可得状态转移方程: d p [ k ] [ j ] = m a x ( d p [ k − 1 ] [ j ] , d p [ k − 1 ] [ j − w [ i ] ] + v [ i ] ) ( i 为 属 于 k 的 物 品 ) dp[k][j]=max(dp[k-1][j],dp[k-1][j-w[i]]+v[i])(i为属于k的物品) dp[k][j]=max(dp[k1][j],dp[k1][jw[i]]+v[i])(ik),代码如下

代码

queue<int>Belong_to_k[n+1];
for(int k=1;k<=n;k++)//n组
	for(int j=V;j>=0;j--)//01背包的模式
		while(!Belong_to_k[k].empty())
		{
			int i=Belong_to_k[k].front();//属于k组的i
			Belong_to_k[k].pop();
			dp[j]=max(dp[j],dp[j-w[i]]+v[i]);
		}

第二层循环必须在第三层循环外才能保证每一组内的物品最多只有一个会被添加到背包中(类似01背包的机制),分组背包也可以采用桶排序的优化,对象是一个组内相同花费的物品

例题

Luogu 1757

题目大意:略

思路:分组背包模板,但是应该能进行常数时间的简化,以样例为例,本代码对第二组重复运行了44次,但总体上还是能通过

代码

#include <iostream>
#include <cstdlib>
#include <cstdio>
#include <vector>
#include <unordered_map>
using namespace std;
int n,m,w[1212],v[1212],dp[12121];
vector<int>Group[12121];
unordered_map<int,bool>Umap;
unordered_map<int,bool>::iterator p;
int main()
{
    scanf("%d%d",&m,&n);
    for(int i=1; i<=n; i++)
    {
        int t;
        scanf("%d%d%d",&w[i],&v[i],&t);
        Group[t].push_back(i);
        Umap[t]=true;
    }
    for(p=Umap.begin(); p!=Umap.end(); p++)
    {
        int k=p->first;
        for(int j=m; j>=0; j--)
        {
            int t=0,len=Group[k].size();
            while(t<len)
            {
                int i=Group[k][t++];
                if(j>=w[i])
                    dp[j]=max(dp[j],dp[j-w[i]]+v[i]);
            }
        }
    }
    printf("%d",dp[m]);
    return 0;
}

HDU 1712

题目大意:给出N种物品,背包容量为M,给出一个N×M的矩阵,对于矩阵A[i][j],含义为对第i种物品付出j的容量得到的价值,求M可得的最大价值

思路:把矩阵的每一行看做一组物品,用分组背包模板即可

代码

#include <iostream>
#include <cstdlib>
#include <cstring>
#include <cstdio>
#include <vector>
using namespace std;
int N,M,dp[12121];
typedef pair<int,int>PR;
vector<PR>Group[12121];
int main()
{
    while(scanf("%d%d",&N,&M)!=EOF&&N&&M)
    {
        for(int i=1; i<=N; i++)
            for(int j=1; j<=M; j++)
            {
                int t;
                scanf("%d",&t);
                Group[i].push_back(make_pair(j,t));
            }
        for(int k=1; k<=N; k++)
            for(int j=M; j>=0; j--)
            {
                int t=Group[k].size();
                for(int i=0; i<t; i++)
                    if(j>=Group[k][i].first)
                        dp[j]=max(dp[j],dp[j-Group[k][i].first]+Group[k][i].second);
            }
        printf("%d\n",dp[M]);
        memset(dp,0,sizeof(dp));
        for(int i=1;i<=N;i++)
            Group[i].clear();
    }
    return 0;
}

有依赖的背包

基本理论

有依赖的背包的基本应用场景为:有一个容量V的背包,有N种各自价值为 v i v_i vi,体积各为 w i w_i wi的物品,这些物品直接存在相互依赖的关系(如选了A必选B),采用怎样的放置物品的顺序与数量才能达到总价值最大?

Luogu P1064为起始,将不依赖于别的物品称为主件,依附于其他物品的称为附件(即只有主件存在才能选附件),因此所有的物品集合为若干主件以及对应附件的整体

按照分组背包的思路,应当把主件以及对应附件视为一个物品组,但是如果有n个附件,整体达到的时间复杂度为2n+1,无法实现状态转移方程

对于这样的一个物品组,每种组合的挑选必然是互斥的,若考虑了完全背包与分组背包中利用桶排序原理的优化,我们就可以对于一个物品组中的物品,在相同费用的情形下,只保留价值最大者

又因为这些组合中都具有主件,所以我们先可以暂且不考虑主件,因为它不会影响结果的相对性大小,而又因为每个附件只能选取一次,所以可以先对附件进行一次01背包,获得0~V-主件容量值的容量的最大价值,即 d p [ 0   V − w [ x ] ] dp[0~V-w[x]] dp[0 Vw[x]],那么物品组被简化成了V-主件容量值+1(主件)这么多物品所构成的物品组,到此可以直接用分组背包的思路解决问题了

这种依赖关系其实可以由森林的形式给出,主件作为树的根节点,限制的是一个物品至多依附于一个物品,且不形成环

解决该问题依然可以用上述的思路,不同的是,附件可能还有其他的物品依附于它,也就是父节点和子节点的关系了,这个时候不能单纯的就一层来使用01背包了,应该先找到叶子节点,然后对同根的叶子节点使用上述思路,使用01背包和父节点构造出物品组,再向上回溯

这便是一种树形DP,特点是每个父节点需要对各子节点的属性进行一次DP来获取自己的相关属性,这已与泛化物品有相通之处,该关系树的每一个子树等价一件泛化物品,求某节点为根的子树对应的泛化物品相当于求其所有子节点对应的泛化物品和

例题

Luogu P1064

题目大意:略

思路:上文已经提及,略

代码

#include <iostream>
#include <cstdlib>
#include <cstdio>
using namespace std;
int n,m,subject_w[32001],subject_v[32001],object_w[32001][3],object_v[32001][3],dp[32001];
//分别存储主件体积、重要度,附件体积、重要度,本题所求为价格×重要度的最大值,二维数组的0项存附件数,1项为第一附件,2项为第二附件
int main()
{
    scanf("%d%d",&n,&m);
    for(int i=1; i<=m; i++)
    {
        int v,p,q;
        scanf("%d%d%d",&v,&p,&q);
        if(q)
        {
            object_w[q][0]++;
            object_w[q][object_w[q][0]]=v;
            object_v[q][object_w[q][0]]=v*p;
        }
        else
        {
            subject_w[i]=v;
            subject_v[i]=v*p;
        }
    }
    for(int i=1; i<=m; i++)
        for(int j=n; j>=subject_w[i]; j--)
        {
            dp[j]=max(dp[j],dp[j-subject_w[i]]+subject_v[i]);//主件必选
            if(j>=subject_w[i]+object_w[i][1])//选第一附件
                dp[j]=max(dp[j],dp[j-subject_w[i]-object_w[i][1]]+subject_v[i]+object_v[i][1]);
            if(j>=subject_w[i]+object_w[i][2])//选第二附件
                dp[j]=max(dp[j],dp[j-subject_w[i]-object_w[i][2]]+subject_v[i]+object_v[i][2]);
            if(j>=subject_w[i]+object_w[i][1]+object_w[i][2])//选第三附件
                dp[j]=max(dp[j],dp[j-subject_w[i]-object_w[i][1]-object_w[i][2]]+subject_v[i]+object_v[i][1]+object_v[i][2]);
        }
    printf("%d",dp[n]);
    return 0;
}

HDU 3449

题目大意:每个例子给出n种盒子,以及身上的总价钱,每种盒子各有自己的价钱且能容纳特定的物品,给出每个盒子自己的价钱以及能装多少物品,还有每件物品的花费与价值,求如何装物品能得到最大价值

思路:对每一个盒子在上一个盒子的基础上进行一次01背包

代码

#include <iostream>
#include <cstdlib>
#include <cstring>
#include <cstdio>
using namespace std;
int n,w,dp[60][200000];
int main()
{
    while(scanf("%d%d",&n,&w)!=EOF)
    {
        memset(dp,0,sizeof(dp));
        for(int i=1; i<=n; i++)
        {
            int box,num;
            scanf("%d%d",&box,&num);
            for(int j=0; j<box; j++)//容量小于盒子容量,直接赋值非法化
                dp[i][j]=-1;
            for(int j=w; j>=box; j--)//将上一次的结果复制下来,作为这次的基础
                dp[i][j]=dp[i-1][j-box];
            for(int j=0; j<num; j++)//对当前盒子对应物品进行01背包
            {
                int c,v;
                scanf("%d%d",&c,&v);
                for(int k=w; k>=c; k--)
                    if(dp[i][k-c]!=-1)
                        dp[i][k]=max(dp[i][k],dp[i][k-c]+v);
            }
            for(int j=0; j<=w; j++)//判断这一个盒子选取之后相对于上一个盒子的状况能否获得更大的价值
                dp[i][j]=max(dp[i-1][j],dp[i][j]);
        }
        printf("%d\n",dp[n][w]);
    }
    return 0;
}

泛化物品

基本理论

泛化物品的基本应用场景为:有一个容量V的背包,有N个物品,对于该物品,如果分配给它的花费为x,那么它的价值为 f ( x ) f(x) f(x),采用怎样的放置物品的顺序与数量才能达到总价值最大?

这个定义类似于HDU 1712,即对同一种物品,给予的费用不同,价值也不同,那么既然HDU 1712可以用分组背包的思路来解决,是否代表这一类问题都与分组背包模型有关联?

对于一个花费为w,价值为v的物品,如果它属于01背包,以泛化物品的策略来思考, ∀ x ∈ [ 0 … V ] , i f x = w , f ( x ) = v , e l s e f ( x ) = 0 \forall x\in[0\dots V],if\quad x=w,f(x)=v,else\quad f(x)=0 x[0V],ifx=w,f(x)=v,elsef(x)=0,如果属于完全背包,仅当 x % w = = 0 x\%w==0 x%w==0 f ( x ) = x / w ∗ v f(x)=x/w*v f(x)=x/wv,其余为0,如果属于多重背包且重复次数最多为n,那么对应的函数为$f(x)=x/w*v仅当x/w\le n,其他情况函数值为0。一个物品组可以看做一个泛化物品,对于一个 0 … V 0\dots V 0V中的x,如果不存在费用为x的物品,则 f ( x ) = 0 f(x)=0 f(x)=0,否则利用桶排序优化的道理, f ( x ) f(x) f(x)为所有费用为w的物品最大值。

对于给定的两个泛化物品a和b,容量已经给定,要求如何从这两个泛化物品中获得最大价值,对于给定的容量x(x可以从0到V),只需要尝试分配,即枚举将x拆分给这两个物品,判断如何分配可得最大值,可得: f ( x ) = m a x ( a [ k ] , b [ x − k ] ) 0 ≤ k ≤ x f(x)=max(a[k],b[x-k])0\le k\le x f(x)=max(a[k],b[xk])0kx

可知f为a与b决定的定义域 0 … V 0\dots V 0V的函数,更抽象一点,f为a、b决定的泛化物品,可得出泛化物品和:a、b为泛化物品,若f满足上给出的方程,则f为a、b的泛化物品和,时间复杂度为 O ( V 2 ) O(V^2) O(V2)

在背包问题中,若将两个泛化物品以其和来代替,不改变答案,对于包括泛化物品的背包,求解过程为求泛化物品之和的过程。

背包问题常见问法&泛化物品

对于背包问题中给出的条件,包括花费、价值、依赖等各属性之间的关系与特定情况下的转换,但这些关系都能通过梳理与融合构成泛化物品,再运用01背包的思路来处理,在各条件已知的前提下,对于背包容量为v的时候,总能获得将物品装入其中的最大值,而这个被装了物品的背包本身又可以被认为是一件物品,即泛化物品,求出该泛化物品的一个子域,可以依据该不同容量对应的取值得到最终结果
对于背包问题的求解,一般求解出该问题的泛化物品,而求泛化物品可以将其分解成若干泛化物品和

输出方案

背包问题求解的是一个最优解,若输出最优解方案,即装入了那些物品,那么就要以终了状态回溯,找到上一个状态,以此类推。可以用类似搜索的方法,当前状态记下上上一状态来自何处,以此类推,以01背包为例,每个状态只来自于两个方向:该物品未选,沿用上一物品结果,该物品选取,加上该物品价值,如果申请一个bool的二维数组,每一个元素对应动态规划的每一项,true代表来自上一层,false表示来自物品选取。当然,也可以直接进行回溯,以终了状态出发,如果该状态与选取结果相等,回溯方向选定,反之亦然

字典序最小最优方案

对于求解字典序最小的最优方案,需要在状态转移时注意策略,以01背包为例,如果存在一个包括编号1的最优方案,所求的字典序最小的答案必然有1,问题被缩小成背包容量为 V − w [ 1 ] V-w[1] Vw[1],物品为 2 … N 2\dots N 2N的问题,反之,则问题变为背包容量为 V V V,物品为 2 … N 2\dots N 2N的问题。为求字典序最小,子问题的顺序应由 1   N 1~N 1 N缩小到 1   N − 1 1~N-1 1 N1,为达到效果,可以逆序排列物品,从第N个物品开始判断,注意若 d p [ i ] [ j ] = = d p [ i − 1 ] [ j ] dp[i][j]==dp[i-1][j] dp[i][j]==dp[i1][j] d p [ i ] [ j ] = = d p [ i − 1 ] [ j − w [ i ] ] + v [ i ] dp[i][j]==dp[i-1][j-w[i]]+v[i] dp[i][j]==dp[i1][jw[i]]+v[i]同时成立,应当选择后者,因为这样字典序更小

求方案总数

对于给定了各条件的背包问题,可以得出装满背包或背包为指定容量的方案总数,对于这类问题,只需将将状态的max改成累和,因为是求方案总数,而不是最优方案总数,所以无需考虑选取的策略是否能得到最大值,以完全背包为例,可得 d p [ i ] [ j ] = ∑ d p [ i − 1 ] [ j ] , d p [ i ] [ j − w [ i ] ] dp[i][j]=\sum dp[i-1][j],dp[i][j-w[i]] dp[i][j]=dp[i1][j],dp[i][jw[i]],初始 d p [ 0 ] [ 0 ] dp[0][0] dp[0][0]为1

最优方案数

求最优方案数同查找方案一样,需要额外申请一个二维数组,在动态规划的求解过程中记录下该状态下的最优方案总数,即在求 d p [ i ] [ j ] dp[i][j] dp[i][j]的同时求出对应的该状态的最优方案数

第K优解

对于求第K优解,有个很容易想到的思路,即求出当前状态的所有解,然后进行排序或查找,找到排名为K的数值即可,该思路在动态规划中大体可以实现,但是还有许多细节需要完善,以01背包为例,对于当前状态,它的解有两个来源,那么就需要对这两个来源进行分开操作,并且所得的值可能相同,而我们所需要的解的序列是肯定不能有重复值的,对于每一个状态,都需要一个额外的一维数组存储当前的解。对于两个来源的值,必定是有序的,因为整个过程就是有序的,那么就可以采用合并有序链表的方法来得出当前状态的所有解,代码如下

代码

int T,N,V,K,dp[1212][32],w[121],v[121],to_be[121],not_to_be[121];
	for(int i=1; i<=N; i++)
        {
            for(int j=V; j>=w[i]; j--)
            {
                for(int y=1; y<=K; y++)
                {
                    not_to_be[y]=dp[j][y];
                    to_be[y]=dp[j-w[i]][y]+v[i];
                }
                to_be[K+1]=not_to_be[K+1]=-1;
                int a=1,b=1,c=1;
                while(a!=K+1&&(to_be[b]!=-1||not_to_be[c]!=-1))
                {
                    if(to_be[b]>not_to_be[c])
                        dp[j][a]=to_be[b++];
                    else
                        dp[j][a]=not_to_be[c++];
                    if(dp[j][a]!=dp[j][a-1])
                        a++;
                }
            }
        }

例题

HDU 2639

题目大意:骨头收集者的升级版,求出给定的第K优解

思路:略

代码

#include <iostream>
#include <cstdlib>
#include <cstdio>
#include <cstring>
using namespace std;
int T,N,V,K,dp[1212][32],w[121],v[121],to_be[121],not_to_be[121];
int main()
{
    scanf("%d",&T);
    while(T--)
    {
        scanf("%d%d%d",&N,&V,&K);
        for(int i=1; i<=N; i++)
            scanf("%d",&v[i]);
        for(int i=1; i<=N; i++)
            scanf("%d",&w[i]);
        for(int i=1; i<=N; i++)
        {
            for(int j=V; j>=w[i]; j--)
            {
                for(int y=1; y<=K; y++)
                {
                    not_to_be[y]=dp[j][y];
                    to_be[y]=dp[j-w[i]][y]+v[i];
                }
                to_be[K+1]=not_to_be[K+1]=-1;
                int a=1,b=1,c=1;
                while(a!=K+1&&(to_be[b]!=-1||not_to_be[c]!=-1))
                {
                    if(to_be[b]>not_to_be[c])
                        dp[j][a]=to_be[b++];
                    else
                        dp[j][a]=not_to_be[c++];
                    if(dp[j][a]!=dp[j][a-1])
                        a++;
                }
            }
        }
        printf("%d\n",dp[V][K]);
        memset(dp,0,sizeof(dp));
        memset(to_be,0,sizeof(to_be));
        memset(not_to_be,0,sizeof(not_to_be));
    }
    return 0;
}

HDU 3810

题目大意:有多个怪物,每个怪物击杀时间,获得金钱已知,并且与之相邻的其他怪兽的位置也给出,对于英雄来说,每次只能选择一只怪兽开始,然后根据相邻位置来移动,判断该英雄能否最后获得大于等于给定值的金钱,如果能,求出击杀时间的最小值

思路:将每个怪物都视为物品的话,该问题就可以转换为对一个彼此相连的物品序列的01背包问题,其次,对于每一件物品,如果耗时大而获得小的物品自然是没有选取的必要的,这个条件可以筛去许多值,但是题目给出的数据过大,无法直接用数组来进行操作,因此需要其他类型的数据结构来支持01背包策略的模拟,优先队列/堆可以满足这个要求

本题充分体现了泛化物品的思想,物品之间的关系根据给出的相邻点,可以抽象成一张,如下图,每个蓝色的圆对应一件物品,红线代表两个物品间相邻,对于物品的选取,我们可以推得,相同钱数下,时间少更优时间不同下,钱数大可能更优,在这两个大前提下,我们可以筛去许多不必要的组合。由题意,我们可以使用DFS将未访问的点遍历,这些点就连成了一个序列,对每个点进行01背包策略。因为数据量过大,使用优先队列,一个存储上个物品的选与不选的情况,另一个记录当前物品执行01背包策略后的结果,选了的物品的结果又成为了新的物品,一如泛化物品,具体见代码

在这里插入图片描述

代码

#include <iostream>
#include <cstdlib>
#include <cstdio>
#include <cstring>
#include <vector>
#include <queue>
using namespace std;
typedef struct Type//设置结构体
{
    int Time,Get;
    Type(int x=0,int y=0)
    {
        Time=x;
        Get=y;
    }
    bool operator<(Type x)const//如果得到的钱数相等,比较时间
    {
        if(Get==x.Get)
            return Time>x.Time;
        return Get<x.Get;//否则比较钱数
    }
} Type;
Type monster[100];
int T,N,M,Ti,Gi,Ki,ans;
bool visited[100];
vector<int>Graph[100];
priority_queue<Type>now,pre;//now记录当前这一层计算出来的状态,
//pre为上一层状态,pre操作完后存入now,默认大顶堆
void DFS(int x)
{
    visited[x]=true;//设置标志
    Type p,tp;
    while(!pre.empty())
    {
        p=pre.top();
        pre.pop();
        now.push(p);//不取该物品
        p.Get+=monster[x].Get;//取该物品
        p.Time+=monster[x].Time;
        if(p.Get>=M)//如果所得已经大于总钱数
        {
            if(ans>p.Time)//看时间是否能被更小的值刷新
            {
                ans=p.Time;
                continue;
            }
        }
        if(ans<=p.Time)continue;
        now.push(p);//取该物品
    }
    tp.Time=ans;
    while(!now.empty())
    {
        p=now.top();
        now.pop();
        if(tp.Time>p.Time)//判断下一个入队点是否钱与时间都少于先前点
            pre.push(p),tp=p;
    }
    int len=Graph[x].size();
    for(int i=0; i<len; i++)//邻接点遍历
    {
        int t=Graph[x][i];
        if(!visited[t])
            DFS(t);
    }
}
int main()
{
    scanf("%d",&T);
    for(int i=1; i<=T; i++)
    {
        scanf("%d%d",&N,&M);
        for(int j=1; j<=N; j++)
        {
            Graph[j].clear();
            visited[j]=false;
            scanf("%d%d%d",&Ti,&Gi,&Ki);
            monster[j].Get=Gi;
            monster[j].Time=Ti;
            while(Ki--)
            {
                int t;
                scanf("%d",&t);
                Graph[j].push_back(t);//记邻接点
            }
        }
        ans=INT_MAX;
        for(int j=1; j<=N; j++)
        {
            if(!visited[j])
            {
                while(!now.empty())now.pop();
                while(!pre.empty())pre.pop();
                pre.push(Type(0,0));
                DFS(j);//对该点DFS,每点进行取或不取的尝试
            }
        }
        printf("Case %d: ",i);
        if(ans!=INT_MAX)
            printf("%d\n",ans);
        else
            printf("Poor Magina, you can't save the world all the time!\n");
    }
    return 0;
}

背包&搜索

在《挑战设计程序竞赛》一书中,对于动态规划的讲解首先是由DFS+记忆化搜索与01背包的关系开始讲述的,基础的01背包与记忆化搜索是互相关联的,而01背包又是所有背包的基础,从哲学的角度上来看,记忆化搜索与背包问题有着或多或少的联系

搜索剪枝

基本的剪枝方法为可行性剪枝与最优性剪枝
前者判断按照当前的搜索路径能否找到可行解,如HDU 3449的解题代码中,对于体积小于物品的背包容量直接进行非法化,剪枝
后者判断按照当前搜索路径能否找到最优解,如完全背包中的桶排序优化,对于同等花费下,拥有最大价值的物品,其他物品的选取显然不能获得最优解,剪枝

子集和问题

给定一个整数集合S和一个整数X,是否存在一个S的子集满足其中元素和为X,集合的大小为N,为求解这个问题,可以将集合划分为两个子集 S 1 、 S 2 S_1、S_2 S1S2,枚举出它们的 2 N / 2 2^{N/2} 2N/2个子集的和,保存到类似哈希表的支持查找的数据结构中,合并两子集的结果,寻找是否存在和为X的集合,对于 S 1 S_1 S1中某个和为 X 1 X_1 X1的子集只需查找 S 2 S_2 S2是否存在 X − X 1 X-X_1 XX1即可,在此基础上其实可以先将两组子集和排序,然后用类似尺取法的策略来选取合适的子集和对,接受的数据范围 N ≤ 42 N\le 42 N42

鱼和熊掌不可兼得?——搜索&DP

动态规划:V不可大,V若大可能为搜索
搜索:N较大(三位数),可能为动态规划

LCS

基本理论

最长公共子序列问题,是动态规划的常用情形,对于给定的两个字符串,求出两者的最长公共子串的长度/字符,首先需要明确子串的概念:对于一个字符串来说,去掉其中特定的字符后剩下的字符串为原字符串的子串,注意,LCS中的子串并不要求连续,如果求最长连续子串只需要修改状态方程即可(不等的时候清零)

对于给出的两个字符串 A = a 1 a 2 a 3 … a w A=a_1a_2a_3\dots a_w A=a1a2a3aw B = b 1 b 2 b 3 … b j B=b_1b_2b_3\dots b_j B=b1b2b3bj,假设它们的子序列为 C = c 1 c 2 c 3 … c y C=c_1c_2c_3\dots c_y C=c1c2c3cy,如果 A w = B j A_w=B_j Aw=Bj,可推得 A w = B j = C y A_w=B_j=C_y Aw=Bj=Cy C y − 1 C_{y-1} Cy1 A w − 1 A_{w-1} Aw1 B j − 1 B_{j-1} Bj1的最长公共子序列,如果不等,C分别为 A w − 1 A_{w-1} Aw1 B j B_{j} Bj A w A_{w} Aw B j − 1 B_{j-1} Bj1最长公共子序列

由此可得状态转移方程:
d p [ i ] [ k ] = m a x ( d p [ i ] [ k − 1 ] , d p [ i − 1 ] [ k ] ) ( A i ! ≠ B k ) dp[i][k]=max(dp[i][k-1],dp[i-1][k])(A_i!≠B_k) dp[i][k]=max(dp[i][k1],dp[i1][k])(Ai!=Bk)
d p [ i ] [ k ] = d p [ i − 1 ] [ k − 1 ] + 1 ( A i = B k ) dp[i][k]=dp[i-1][k-1]+1(A_i=B_k) dp[i][k]=dp[i1][k1]+1(AiBk)

如果想要获得这个最长公共子串,只需要记录当前dp中当前状态值来自于哪里即可,之后采用回溯法找到字符相等的位置

本博客讲述的较浅,具体LCS的相关解释可以查阅参考文献

基本代码实现

求值

#include <iostream>
#include <cstdlib>
#include <cstdio>
#include <cstring>
using namespace std;
char A[1212],B[1212];
int dp[1212][1212],lena,lenb;
int main()
{
    scanf("%s",A+1);
    getchar();
    scanf("%s",B+1);
    lena=strlen(A+1),lenb=strlen(B+1);
    for(int i=1; i<=lena; i++)
        for(int j=1; j<=lenb; j++)
            if(A[i]==B[j])
                dp[i][j]=dp[i-1][j-1]+1;
            else
                dp[i][j]=max(dp[i-1][j],dp[i][j-1]);
    printf("%d",dp[lena][lenb]);
    return 0;
}

输出结果

#include <iostream>
#include <cstdlib>
#include <cstdio>
#include <cstring>
using namespace std;
char A[1212],B[1212];
int dp[1212][1212],lena,lenb,flag[1212][1212];
void Print(int i,int j)
{
    if(i==0||j==0)return;
    if(flag[i][j]==0)
    {
        Print(i-1,j-1);
        printf("%c",A[i]);
    }
    else if(flag[i][j]==1)
        Print(i-1,j);
    else
        Print(i,j-1);
}
int main()
{
    scanf("%s",A+1);
    getchar();
    scanf("%s",B+1);
    lena=strlen(A+1),lenb=strlen(B+1);
    for(int i=1; i<=lena; i++)
        for(int j=1; j<=lenb; j++)
            if(A[i]==B[j])
                dp[i][j]=dp[i-1][j-1]+1;
            else
            {
                dp[i][j]=max(dp[i-1][j],dp[i][j-1]);
                if(dp[i-1][j]>=dp[i][j-1])
                    flag[i][j]=1;//标记
                else
                    flag[i][j]=-1;
            }
    Print(lena,lenb);//只输出一个结果
    return 0;
}

优化

LCS问题很类似于01背包,因为对于每个字符都涉及到两种选择,即选与不选,那么,LCS是否能类似于01背包,只使用滚动数组就完成求解过程?

答案当然是Yes

对于dp数组中的每一个状态,我们可以发现它的来源不外乎三者: d p [ i − 1 ] [ j − 1 ] 、 d p [ i ] [ j − 1 ] 、 d p [ i − 1 ] [ j ] dp[i-1][j-1]、dp[i][j-1]、dp[i-1][j] dp[i1][j1]dp[i][j1]dp[i1][j],也就是说,它的前置状态有三种,那么我们只需要保留两行的数据,利用滚动数组的原理模拟动态规划的过程即可,代码实现如下:

代码

#include <iostream>
#include <cstdlib>
#include <cstdio>
#include <cstring>
using namespace std;
char A[1212],B[1212];
int dp[2][1212],lena,lenb,now,pre,i,j;
int main()
{
    scanf("%s",A+1);
    getchar();
    scanf("%s",B+1);
    lena=strlen(A+1),lenb=strlen(B+1);
    for(now=1,pre=0,i=1; i<=lena; i++)
        for(swap(now,pre),j=1; j<=lenb; j++)
            if(A[i]==B[j])
                dp[now][j]=dp[pre][j-1]+1;
            else
                dp[now][j]=max(dp[pre][j],dp[now][j-1]);
    printf("%d",dp[now][lenb]);
    return 0;
}

求解最长公共子串的算法时间复杂度一般为 O ( n 2 ) O(n^2) O(n2),这样的时间复杂度能应对大多数题目,但是如果数据量达到104及以上的级别,总时间消耗就无法达到题目要求,因此需要更快的算法, L I S LIS LIS算法应运而生

使用 L I S LIS LIS算法有两个前提条件:将LCS转换为LIS采用LIS的 O ( n log ⁡ n ) O(n\log n) O(nlogn)的算法,关于LIS算法以及什么是LIS请参考后面的文献,在此不再赘述,只谈一下转换与算法的问题

LIS 的O(n\log n)算法

LIS的应用对象主要为数字,毕竟是最长上升子序列, O ( n 2 ) O(n^2) O(n2)的算法就不多说了,这里讲一下 O ( n log ⁡ n ) O(n\log n) O(nlogn)的算法,翻阅了很多网上的资料,思考了许久才悟得三分,在此写下拙见

对于给定的一个数字序列,我们的任务是获取其最长的上升子序列,假设序列有n个元素,而这个上升子序列的长度必然属于1~n的范围内,当然不是每一个值都可行,对于每一个可行值,保留其构造出的最小末尾值,由动态规划的算法可知,当可行值为x时,x-1必属于可行值,可行值为x-1的末尾值越小,可行值x越可能实现,因此我们需要动态地维护各可行值的最小末尾值,开辟一个数组存取这些最小末尾值,对于输入的data[i],如果它大于最大可行值的最小末尾值,那么最大可行值+1,数组下标对应值为data[i],否则在各最小末尾值中查找首个大于data[i]的值(从小到大),将它更新为data[i],这样可以使同等可行值的条件下,末尾元素更小,为找到这个大于data[i]的值,因为所维护的最小末尾值数组有序,所以可以使用二分查找

在学习这个算法的时候,有几个问题始终困扰着我,一是为什么最小末位值序列一定有序,二是更新了最小末尾值后是否会对长度最大值产生影响,三是存贮的最小末位值是否为最长上升子序列,经过对资料的查阅与整理,再加上自己的理解,遂给出这三点的证明,希望能帮助读者更好理解

证明有序

对于每一个录入的数值,如果大于最大可行值的最小末尾值,它必然大于前面所有元素,因为该最小末尾值必大于前面所有元素(可行值最大,则对应的LIS末尾值必定为整个序列的最大值),如果小于,对于找到的第一个大于它的值,假设为位置i,i之前必小于录入值,i之后必大于录入值

证明不会对长度最大值产生影响

在已经得出长度最大值的情况下,录入的值只会改变数组的内容,而不会已经求出的最大值

证明不一定为最长上升子序列

对于这个问题的证明,我们需要明确一个概念,最小末尾值数组存储的内容代表了什么?代表了以该值为长度i的末尾数使得录入的数更可能延长这个LIS,由此可知,我们存储的其实是最可能解,而求解的过程是忽视位置i存储的元素与i之后元素的在原序列中的位置关系的,举个例子,现在已知存在长度为i-1的LIS,min_tail[i-1] (最小末尾值数组)的值为x,而min_tail[i]值为y,y在原序列中对应的位置大于x,现在录入了一个数z,z的首个大于等于自身的数恰为x,那么按照算法原理,x被替换成z,但z在原序列的位置显然大于y的位置,这个序列必然不是LIS

探讨完这些问题后,LIS的难点应该解决了大半,下面给出代码

代码

#include <iostream>
#include <cstdlib>
#include <cstdio>
#include <algorithm>
using namespace std;
int n,low[1212],ans,data[1212];
int main()
{
    scanf("%d",&n);
    for(int i=0;i<n;i++)
        scanf("%d",&data[i]);
    for(int i=0;i<=n;i++)
        low[i]=121212;
    for(int i=0;i<n;i++)
    {
        if(ans==0||data[i]>=low[ans])
           low[++ans]=data[i];
        else
        {
            int pos=lower_bound(low+1,low+1+ans,data[i])-low;
            low[pos]=data[i];
        }
    }
    printf("%d",ans);
    return 0;
}

LCS转换成LIS

花了如此大的篇幅讲述LIS,自然有它的价值,接下来就讲解一下LCS转换成LIS的算法,只有在了解了LIS的 O ( n log ⁡ n ) O(n\log n) O(nlogn)的算法之后,才能更好的理解LCS的 O ( n log ⁡ n ) O(n\log n) O(nlogn)的算法

首先给出一个例子

A = l b w n b , B = b w a f n c b a t n A=lbwnb,B=bwafncbatn A=lbwnb,B=bwafncbatn,如果以A串为匹配对象,找到A串中的每个字符在B串中的位置,如果出现多次,按照降序存储(为什么?)

那么可以得出这样的结果: l = { ∅ } , b = { 1 , 7 } , w = { 2 } , n = { 4 , 10 } l=\{\empty\},b=\{1,7\},w=\{2\},n=\{4,10\} l={},b={1,7},w={2},n={4,10},以b为例,得出的结果意味着A中的b可以与B位于位置1的b匹配,也可以与位置7匹配,显然,与位置1匹配,A中的其他字符有更大的可能性在B中匹配到相等的字符,进一步构成更长的串,B串便可以变成这样的序列: 1 , 7 , 2 , 4 , 10 1,7,2,4,10 1,7,2,4,10,对于每个字符,我们需要找到大于前面已匹配成功字符的最小值,例如如果选了选择了B串第一个b,那么w就必须选2,n在4和10中抉择选4,如果选择了第二个b,w不可选,n只能选10

这样的操作方式与LIS问题如出一辙,因此LCS问题被转换成了LIS问题,时间复杂度得到了优化,下面给出代码

代码

例题

HDU 1503

题目大意:给出两个字符串,公共子串只输出一次,输出两个字符串的合成串

思路:

代码

#include <iostream>
#include <cstdlib>
#include <cstdio>
#include <cstring>
using namespace std;
int lena,lenb,dp[121][121],lenc;
char A[121],B[121],C[424];
int main()
{
    while(scanf("%s%s",A+1,B+1)!=EOF)
    {
        lena=strlen(A+1),lenb=strlen(B+1),lenc=0;
        for(int i=1; i<=lena; i++)
            for(int j=1; j<=lenb; j++)
            {
                if(A[i]==B[j])
                    dp[i][j]=dp[i-1][j-1]+1;
                else
                {
                    if(dp[i-1][j]>=dp[i][j-1])
                        dp[i][j]=dp[i-1][j];
                    else
                        dp[i][j]=dp[i][j-1];
                }
            }
        while(1)
        {
            if(A[lena]==B[lenb])//如果是公共字符,直接放入
            {
                C[++lenc]=A[lena];
                lena--,lenb--;
            }
            else if(dp[lena-1][lenb]>dp[lena][lenb-1])//代表A串独有这个字符,
            //且lena对应的字符在lenb的后面
                C[++lenc]=A[lena--];
            else
                C[++lenc]=B[lenb--];
            if(!lena)
            {
                for(int i=lenb; i>=1; i--)
                    C[++lenc]=B[i];
                break;
            }
            if(!lenb)
            {
                for(int i=lena; i>=1; i--)
                    C[++lenc]=A[i];
                break;
            }
        }
        for(int i=lenc; i>=1; i--)
            printf("%c",C[i]);
        putchar('\n');
    }
    return 0;
}

Luogu P2516

题目大意:略

思路:本题属于背包九讲中最优方案数的问题,对于每一个状态,它必定对应着至少一种可实现的方案,通过状态转移方程我们可以知道,每个状态都是由上一个状态转移到当前状态的,因此,该状态的最优方案数应当加上到达上一状态的最优方案数,因此,在代码实现当中申请一个数组来记录当前状态的最优方案数即可,如果来自 d p [ i − 1 ] [ j ] dp[i-1][j] dp[i1][j],加上对应的值,其余两个状态同理,但是,有一种情况特殊,如果三者状态值都相等,就需要减去属于 [ i − 1 ] [ j − 1 ] [i-1][j-1] [i1][j1]的方案数,这是为什么呢?如图

在这里插入图片描述
三种状态都相等的时候,表明 d p [ i − 1 ] [ j ] 、 d p [ i ] [ j − 1 ] dp[i-1][j]、dp[i][j-1] dp[i1][j]dp[i][j1] d p [ i − 1 ] [ j − 1 ] dp[i-1][j-1] dp[i1][j1]转换而来,因此 d p [ i ] [ j ] dp[i][j] dp[i][j]上一次比对,即 d p [ i − 1 ] [ j − 1 ] dp[i-1][j-1] dp[i1][j1]有同样的解决方案,根据前面的设定,这里重复加了一次 d p [ i − 1 ] [ j − 1 ] dp[i-1][j-1] dp[i1][j1]对应的解决方案(集合论),需要减去,最后还有取余
代码

#include <iostream>
#include <cstring>
#include <cstdio>
#include <cstdlib>
#define mod 100000000
using namespace std;
int dp[2][5050],lena,lenb,way[2][5050],now;
char A[5050],B[5050];
int main()
{
    scanf("%s%s",A+1,B+1);
    lena=strlen(A+1)-1,lenb=strlen(B+1)-1;
    for(int i=0; i<=lenb; i++)//初始化方案数
        way[0][i]=1;
    way[1][0]=1;
    for(int i=1; i<=lena; i++)
    {
        now^=1;
        for(int j=1; j<=lenb; j++)
        {
            way[now][j]=0;
            dp[now][j]=max(dp[now^1][j],dp[now][j-1]);//dp
            if(A[i]==B[j])//dp
                dp[now][j]=dp[now^1][j-1]+1;
            if(dp[now][j]==dp[now^1][j])way[now][j]+=way[now^1][j];//如果来自i-1,j
            if(dp[now][j]==dp[now][j-1])way[now][j]+=way[now][j-1];//如果来自i,j-1
            if(dp[now][j]==dp[now^1][j]&&dp[now][j]==dp[now][j-1]&&dp[now^1][j-1]==dp[now][j])way[now][j]-=way[now^1][j-1];//状态都相等去掉重复计算的数值
            if(A[i]==B[j]&&dp[now][j]==dp[now^1][j-1]+1)way[now][j]+=way[now^1][j-1];
            way[now][j]=(way[now][j]+mod)%mod;//取余
        }
    }
    printf("%d\n%d",dp[now][lenb],way[now][lenb]);
    return 0;
}

Luogu P1439

题目大意:略

思路:将LCS转换为LIS的标准模板,因为是全排列,所以每个数字只会出现一次,这样可以大大简化算法的时间复杂度

代码

#include <iostream>
#include <cstdlib>
#include <cstdio>
#include <cstring>
#include <algorithm>
using namespace std;
int n,A[121212],low[121212],loc[121212],ans;//注意数组规模
int main()
{
    scanf("%d",&n);
    for(int i=1; i<=n; i++)
        scanf("%d",&A[i]);
    for(int i=1; i<=n; i++)
    {
        int t;
        scanf("%d",&t);
        loc[t]=i;//记录A字符在B对应的位置
    }
    for(int i=1; i<=n; i++)
        A[i]=loc[A[i]];//重复利用存储空间,使A存储每个字符在B的位置
    for(int i=1; i<=n; i++)
    {
        if(ans==0||A[i]>low[ans])
            low[++ans]=A[i];
        else
        {
            int pos=lower_bound(low+1,low+1+ans,A[i])-low;
            low[pos]=A[i];
        }
    }
    printf("%d",ans);
    return 0;
}

后缀树

树形DP&树形背包

区间DP

数位DP

总结

参考文献

  1. 背包九讲——全篇详细理解与代码实现
  2. 背包九讲
  3. 《挑战程序设计竞赛》
  4. 单调队列详解
  5. 多重背包O(N*V)算法详解(使用单调队列)
  6. 多重背包:经典DP问题( 基本/二进制优化/单调队列优化 )
  7. hdu 3535(背包综合题)
  8. HDU3810 Magina(搜索+用优先队列模拟01背包)经典
  9. 三、动态规划算法解最长公共子序列LCS问题(2011.12.13重写)
  10. 程序员编程艺术第十一章:最长公共子序列(LCS)问题
  11. 最长公共子序列、最长公共子串的输出
  12. Advanced Fruits(最长公共子序列)
  13. 最长公共子序列(滚动数组优化)
  14. P1439 【模板】最长公共子序列 题解
  15. 最长上升子序列 (LIS) 详解+例题模板 (全)
  16. 最长公共子序列O(nlogn)
  17. P2516 [HAOI2010]最长公共子序列 题解
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值