hdu4778 状态压缩dp+博弈

好难的一道题目,可能跟自己以前很少做博弈有关吧。

题意很好理解,两者都在最优策略下取包裹,问最后得到的分数差是多少。背包21个,惯用思路状压dp,这点倒是很容易想到。

可是一般的状压dp只让求一方的最优策略,没有考虑双方的情况,如何才能保证两者都是在最优策略下取的背包呢?

我们可以很容易发现,对于任何一种局势,无论两者中谁碰到,最优策略均只有一种,即两个人完全按照相同的方式去取背包。

之后,我们又可以发现,对于任何一种局势,无论两者谁取,目标都是让自己比对方得到的分数更大,最后求的也是先手减后手的得分。

这样,我们可以得到状态的表示,一个二进制数每位代表一个背包的状态,0表示这个背包已经取过,1表示还没取,这个状态的含义便是,当已经取完这么多背包之后,剩余的没取的背包能够造成的先手-后手的最大值。然后便惯用套路去更新,这里要注意,末状态是所有背包都没取,即待取状态的时候,起始状态便是所有背包都取完了(0状态),无论谁面对这种局势都无法从剩余背包中得到宝石,即先手-后手等于0。更新的时候,对当前状态,将一个已取的背包设为未取的状态,即在这种情况下多了一个背包可以取,可以计算出取了这个背包能得多少分,如果得分大于0,得到的分数加上当前状态先手-后手的得分便可以成为是新状态的先手-后手的得分,如果得分等于0,那么在新状态时如果取了这个背包,就要进行换手,换手之后的先手变成对方,即当前状态相当于记录的是对方-己方的最大得分(本来是先手-后手,但由于换手了,取了之后我变成后手,即dp[i]为对方-己方的最大得分),那么己方-对方的得分便是取负,这样得到一个可行的新状态先手-后手的得分。依次这样求下去不断取先手-后手最大值,推到末状态的时候,记录的也是先手-后手的最大值,将先手设为alice,便是alice面对这种局势时能比对方最多的多少分,总分数一定,那么我越多对方越少,即是alice先手的情况下最优策略下的alice-bob的分数。

注意理解的部分有:

0)。状态中0表示已经取过,取过的部分只对炉子中剩余宝石的数量有影响;1表示没取过,当前状态可以从1里面选一个取而得到下一个状态。

1)。先手-后手的分数,无论什么时候dp[i]都是记录的面对当前局势先手-后手的得分。换手了之后相当于对方变先手我成后手,故要取负。

2)。dp的时候是从小到大dp,但是实际上取的时候是从大向小取的,即先手可以控制局面,先手可以控制下一步达到什么局面,因此一个局面是从所有它取一个背包之后能造成的局面中最有利自己的一个推出的,因此可以一直取最大值。(相当于我一直控制一个局面,这个局面能使得我比对方得分更多,因为面对相同局势两者采取的路径完全一样,先手完全可以猜测出我新造成的局面到底能给我带来多少好处,求这个好处的过程便是dp的过程)。

这个题目还可以记录每个状态先手最多能获得多少分数,那样写的话写搜索比较好,因为要记录每个状态总共能获得多少宝石(换手的时候更新时先手变后手,后手得变先手,后手得到的宝石数量等于总数减去先手得到的宝石数量),用dp写的话要一步一求总分比较麻烦,用搜索递归的时候总数直接减去已经得到的便是当前状态总数。

代码:

#include <iostream>
#include <cstring>
#include <cstdio>

using namespace std;

int dp[(1<<21)+10][9];
int g,b,s;
int v[25][10];
int a[10];

int max(int a,int b)
{
    return a>b?a:b;
}

int main()
{
    while (scanf("%d%d%d",&g,&b,&s)&&g+b+s)
    {
        memset(v,0,sizeof(v));
        memset(a,0,sizeof(a));
        int sum=0;
        for (int i=0;i<b;i++)
        {
            int n;
            scanf("%d",&n);
            while (n--)
            {
                int a1;
                scanf("%d",&a1);
                v[i][a1]++;
                a[a1]++;
                sum+=a[a1]/s;
                a[a1]%=s;
            }
        }
        int mm=(1<<b)-1;
        for (int i=1;i<=g;i++)
            dp[0][i]=a[i];      //全部取完炉子的状态
        dp[0][0]=0;
        for (int i=1;i<=mm;i++)
            dp[i][0]=-1000000;
        for (int i=0;i<=mm;i++)
        {
            for (int j=0;j<b;j++)
            {
                int x=(1<<j);
                if ((i&x)==0)   //如果j取过了
                {
                    int gs=0;
                    for (int k=1;k<=g;k++)  //从没取j到取j总过得到多少宝石
                    {
                        a[k]=dp[i][k]-v[j][k];
                        while (a[k]<0)  //说明取的时候获得了宝石
                        {
                            gs++;
                            a[k]+=s;
                        }
                        dp[i^x][k]=a[k];
                    }
                    if (gs>0)   //如果大于0,不换手,先手+gs-后手=gs+dp[i][0]
                        dp[i^x][0]=max(dp[i^x][0],gs+dp[i][0]);
                    else    //换手,新得到的分数等于0,原来的先手-后手变成后手-先手,取负即可
                        dp[i^x][0]=max(dp[i^x][0],-dp[i][0]);
                }
            }
        }
        printf("%d\n",dp[mm][0]);
    }
}



评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值