可逆背包

 

以前一直咕咕咕,直到又又又碰到了之前没有补的题

该学的还是不能拖

 


 

只有很少几类背包才具有可逆性质(一般都是01背包)

比如考虑这样一个问题:

给定$n$个物品,每件物品有重量$w_i$;共有$n$次询问,第$i$次询问中我们想要求 不选物品$i$时 共有多少种选法使得总重量为$W$

如果我们对于每个询问依次求背包问题,那么单次需要$O(nW)$的DP,于是整体复杂度为$O(n^2W)$

而退背包能够让我们不需要对于每个询问都进行完整的DP,而是用$O(W)$抹去选择了物品$i$的方案数,那么整个过程需要进行一次$O(nW)$的DP和$n$次$O(W)$的退背包/加背包的过程,整体复杂度就降到了$O(nW)$

 

其实上面所描述的就是这道题目:BZOJ 2287 (【POJ Challenge】消失之物)

我们先对$n$个物品做一次01背包,得到$dp[i]$表示总容量为$i$的方案共有多少种

现在禁止选择第$i$个物品,我们想求$ans[j]$表示在该限制下总容量为$j$的方案共有多少种

那么就需要删去那些选择了物品$i$的方案,我们不妨认为最后选择了物品$i$(即在做01背包时,不是从物品$1$选到物品$n$,而是依次选择物品$1,2,...,i-1,i+1,...,n,i$)

此时,对于$j<w[i]$,总容量为$j$的方案显然不可能选择物品$i$,故$ans[j]=dp[j]$

而对于$w[i]\leq j\leq m$,此时总容量为$j$的方案就有可能选择了物品$i$,那么选择了物品$i$的方案必然是从总容量为$j-w[i]$、且未选择过物品$i$的方案转移过来的,而这个方案数恰好为已经计算出来的$ans[j-w[i]]$($ans[j]$是从小到大计算的),于是有$ans[j]=dp[j]-ans[j-w[i]]$

#include <cstdio>
#include <algorithm>
using namespace std;

const int N=2005;

int n,m;
int w[N];
int dp[N],ans[N];

int main()
{
    scanf("%d%d",&n,&m);
    for(int i=1;i<=n;i++)
        scanf("%d",&w[i]);
    
    dp[0]=1;
    for(int i=1;i<=n;i++)
        for(int j=m;j>=w[i];j--)
            dp[j]=(dp[j]+dp[j-w[i]])%10;
    
    for(int i=1;i<=n;i++,putchar('\n'))
    {
        for(int j=0;j<=m;j++)
            ans[j]=dp[j];
        for(int j=w[i];j<=m;j++)
            ans[j]=(ans[j]-ans[j-w[i]]+10)%10;
        for(int j=1;j<=m;j++)
            putchar(ans[j]+'0');
    }
    return 0;
}

 

OpenCup 18242A / Nowcoder 890A  ($Blackjack$,2019牛客暑期多校Day10)

Problem G. Blackjack
Input file: standard input
Output file: standard output
Time limit: 4 seconds
Memory limit: 256 mebibytes

As the saying goes, “if you are hesitant, you will lose the game; if you are decisive, you will die in vain.”

Calabash doesn’t want to lose the game, neither to die in vain. But, this is not important, because he
doesn’t have enough money to buy the game.

Fortunately, Roundgod, the banker of a casino, gives him a chance to fulfill his dream. Roundgod
promises to buy him the game if he could win in one round of Blackjack. The Blackjack involves a deck
of playing cards, each has some points on it. It is carried out as follows: Roundgod gives a number a
and tells Calabash the number of points on each of the cards. Then, Calabash begins to draw cards,
according to the following procedure:

1. the deck of all cards is shuffled uniformly at random;

2. Calabash could repeatedly draw a card from the deck (he immediately knows the number of the
points on the card after drawing the card), or declare to stop drawing at any moment;

3. if, after drawing any card, the total number of points on cards Calabash drawn exceeds a limit b,
he loses immediately;

4. otherwise, after Calabash stops drawing, he wins if and only if the total number of points on cards
he drew is greater than a.

Calabash wants to know the probability to win if he plays optimally so that he can obtain his favorite
game. Please help him to calculate the probability.

Input

There are two lines in the input. The first line consists of three integers n, a, b
(1 ≤ n ≤ 500, 1 ≤ a < b ≤ 500), denoting the number of cards in the deck, the number given by
Roundgod, and the limit to the total number of points, respectively.

The second line contains n integers x_1, x_2, · · · , x_n (1 ≤ x_i ≤ 500), denoting the numbers of points of
these cards. It is guaranteed that \sum_{x_i} > b.

Output

Output the answer within an absolute error of no more than 10^{-6}.

Examples

standard input #1
5 2 4
1 1 1 5 5

standard output #1
0.100000000000

standard input #2
5 2 4
1 1 1 3 5

standard output #2
0.450000000000
题面

会了上面一道题后其实还远远不算了解基本概念,通过这道题目可以巩固对可逆背包的印象

首先,我们可以用一个DP方程表示状态

$dp[i][j]$表示,选择$i$张牌、总点数恰为$j$的方案数

那么显然需要尝试用每张牌进行转移;其中,用第$k$张牌的转移是$dp[i][j]\rightarrow dp[i+1][j+x[k]]$

最后,已选的牌可以随意排列,未选的牌可以随意排列,乘上这两个排列数再除以$n!$就能得到一个结果

这样看似能解决这个问题了?其实并不

第二个样例就是hack:假如$a=2,b=4$,有两张牌$1,3$,那么已选牌的出现顺序只可能为$1,3$,不可能为$3,1$(因为第一张是$3$时已经胜利并结束了)

于是只能考虑将一张牌从背包中退掉,此时已选(不包含退掉的牌)、未选的牌均是可以任意排列的

那么考虑退背包的过程:

先枚举退掉的牌$k$,然后用$tmp[i][j]$表示,选择$i$张牌(禁止选$k$)、总点数恰为$j$的方案数

如何转移呢?考虑$dp[i][j]$有两个部分组成:$tmp[i][j]$(未选$k$)和$tmp[i-1][j-x[k]]$(选了$k$),于是能够得到$tmp[i][j]=dp[i][j]-tmp[i-1][j-x[k]]$;显然需要按照$i$从小到大的顺序对$tmp$数组进行计算

不过得出的$tmp[i][j]$只是已选的、可随意排列的牌,还必须在最后选$k$,于是$\sum_{j=a+1-x[k]}^{b-x[k]}tmp[i][j]$对于答案的贡献还需要乘上$\frac{j!\cdot (n-j-1)!}{n!}$的系数

需要用long double类型来存储DP的中间结果

#include <cstdio>
#include <algorithm>
using namespace std;

typedef long double ld;
const int N=1005;

int n,A,B;
int x[N];

ld dp[N][N],tmp[N][N];

int main()
{
    scanf("%d%d%d",&n,&A,&B);
    for(int i=1;i<=n;i++)
        scanf("%d",&x[i]);
    
    dp[0][0]=tmp[0][0]=1.0;
    for(int i=1;i<=n;i++)
        for(int j=i-1;j>=0;j--)
            for(int k=B;k>=x[i];k--)
                dp[j+1][k]+=dp[j][k-x[i]];
    
    ld ans=0.0;
    for(int i=1;i<=n;i++)
        for(int j=0;j<=n;j++)
        {
            for(int k=0;k<=B;k++)
                tmp[j][k]=dp[j][k];
            for(int k=x[i];k<=B;k++)
                tmp[j][k]-=(j>0?tmp[j-1][k-x[i]]:0);
            
            ld sum=0.0;
            for(int k=max(0,A+1-x[i]);k<max(0,min(A+1,B+1-x[i]));k++)
                sum+=tmp[j][k];
            
            for(int k=n-j-1;k>=1;k--)
                sum=sum*k/(k+j+1);
            ans+=sum/(j+1);
        }
    
    printf("%.8f\n",(double)ans);
    return 0;
}
View Code

 

Codeforces 1111D  ($Destroy\ the\ Colony$)

虽然题目的输入$n=1\times 10^5$看起来很吓人,但实际上仅包含大小写字母,即至多$52$个物品、每个物品的重量为其在字符串中的出现次数$x_i$

那么显然是可以离线将$52^2$种答案计算出来的,于是$q$也没什么用了

考虑$dp[i]$表示选择总重量为$i$的物品共有多少种方案

由于题目要求要选出$2$组,每组的重量均为$\frac{n}{2}$,那么不妨认为$dp[n/2]$就是第一组的选法(第二组的选法随之而确定)

可以发现,对应一种选法,具体的排列总数需要再乘上一个系数,为$W=\frac{(n/2)!\cdot (n/2)!}{\prod x_i}$;这里的乘法逆元可以用递推求逆元的方法预处理

如果询问的两个字符时相同的,那么答案就是$W\cdot dp[n/2]$

当两个字符不同时,就不能仅仅用$dp$数组处理了,而是需要枚举这两个字符,并将其从背包中退掉;记退掉第一个字符后的数组为$tmp1$,退掉第一、第二个字符后的数组为$tmp2$,那么$tmp2[n/2]$表示的就是 不包含这两个字符的一组 有多少种选法,在此时$tmp2[n/2]$可以是前一半或后一半(与上面不太一样),故答案是$2W\cdot tmp2[n/2]$

#include <cstdio>
#include <cstring>
#include <algorithm>
using namespace std;

typedef long long ll;
const int MOD=1000000007;
const int N=100005;
const int M=55;

int n,q;
char s[N];

int x[M];
inline int rnk(char ch)
{
    return (ch>='a' && ch<='z'?ch-'a'+1:ch-'A'+27);
}

ll rev[N];

int dp[N],tmp1[N],tmp2[N];
int ans[M][M];

int main()
{
    scanf("%s",s+1);
    n=strlen(s+1);
    
    for(int i=1;i<=n;i++)
        x[rnk(s[i])]++;
    
    rev[1]=1;
    for(int i=2;i<=n;i++)
        rev[i]=(MOD-MOD/i)*rev[MOD%i]%MOD;
    
    ll W=1;
    for(int i=1;i<=n/2;i++)
        W=W*i%MOD*i%MOD;
    for(int i=1;i<M;i++)
        for(int j=1;j<=x[i];j++)
            W=W*rev[j]%MOD;
    
    dp[0]=1;
    for(int i=1;i<M;i++)
        for(int j=n;j>=x[i];j--)
            if(x[i]>0)
                dp[j]=(dp[j]+dp[j-x[i]])%MOD;
    
    for(int i=1;i<M;i++)
        ans[i][i]=W*dp[n/2]%MOD;
    for(int i=1;i<M;i++)
    {
        for(int j=0;j<=n;j++)
            tmp1[j]=dp[j];
        for(int j=x[i];j<=n;j++)
            tmp1[j]=(tmp1[j]-tmp1[j-x[i]]+MOD)%MOD;
        
        for(int j=i+1;j<M;j++)
        {
            for(int k=0;k<=n;k++)
                tmp2[k]=tmp1[k];
            for(int k=x[j];k<=n;k++)
                tmp2[k]=(tmp2[k]-tmp2[k-x[j]]+MOD)%MOD;
            
            ans[i][j]=ans[j][i]=2*W*tmp2[n/2]%MOD;
        }
    }
        
    scanf("%d",&q);
    while(q--)
    {
        int x,y;
        scanf("%d%d",&x,&y);
        printf("%d\n",ans[rnk(s[x])][rnk(s[y])]);
    }
    return 0;
}
View Code

 

HDU 6460  ($Renaissance\ Past\ in\ Nancy$,$2018ICPC$沈阳)

参考了:qkoqhh - 2018沈阳M(可逆背包+线性优化完全背包+生成函数)

题目太文艺了...用人话翻译一下就是:$n$种物品,每种有$a_i$个、大小为$b_i$,有$m$个在线询问,每个询问为 从$l_i$物品选到$r_i$物品、总容量不超过$c_i$有多少种选法

多重背包可以用单调队列优化到$O(nV)$,具体可见:顾z - 背包问题入门(单调队列优化多重背包 写的非常好

(咕咕中,需要补)

posted @ 2020-06-07 18:45  LiuRunky  阅读(1449)  评论(0)    收藏  举报