UVa10817 Headmaster's Headache【状压dp(递推写法)】

失踪人口回归

前言

多日不见,博主在偷懒百忙之中,又重操旧业,开始写写博客了,这次呢,主要是因为一道非常恶心好的题。
嗯,就是标题中的UVa10817了。(可能UVa的加载有点慢,这里帖vjudge上面的地址)
这道题呢,相信大家也不陌生,就是某本算法竞赛书上面的例题了。
我第一眼没去看它的分析,而是自己想了想我们之前做的几道状压dp,
我觉得这不就是一道三进制的状压dp吗??
书上写的是记忆化搜索,同学也都差不多用的记忆化搜索,但是我就是不服了,
难道我就是不能自己用递推写法A一道状压dp???

(噩梦开始)

分析

首先呢, d[3s1] d [ 3 s − 1 ] 就是我们的目标状态,而初始状态,因为有固定教师,就有点麻烦了

for(int i=1;i<=m;i++)
    {
        c[i]=geti();
        while( (k=geti()) )
        {
            st[k]++;
            if(pp) break;
        }
    }
for(int i=1;i<=s;i++)
        S += Pow3[i-1]*min(2,st[i]);

其中,S是我们的初始状态,geti()是读入整数的函数

写到这里,我不得不说说这道题的读入了。
直接的scanf肯定是不能满足的了,有的人就用了fgets 或者 cin.getline。
我还是想了想,用几分钟写了一个这道题专用的读入
见下方

bool pp;
int geti()
{
    int x,f=1;char c; pp=0;
    while((c=getchar())<'0' || c>'9')
    {
        if(c == '-') f=-1;
        if(c == -1) return -1;
    }
    x = c-'0';
    while((c=getchar())>='0' && c<='9') x=(x<<3)+(x<<1)+c-'0';
    if(c == '\n') pp=1;
    return x*f;
}

写完这玩意,又花了几十分钟写了个状压dp的基本模型(当然是三进制版的)

inline int K(int x,int y)
{
    int res=0;
    for(int i=0;i<10;i++)
        res += Pow3[i]*min(2,x%Pow3[i+1]/Pow3[i] + y%Pow3[i+1]/Pow3[i]);
    return res;
}

void Solve()
{
    int S=0; d[0] = 0;
    for(int i=1;i<=s;i++)
        S += Pow3[i-1]*min(2,st[i]);

    for(int S1=0;S1for(int i=1;i<=n;i++)
            d[K(S1,V[i])] = min(d[S1] + a[i],d[K(S1,V[i])]);

    ans = d[ Pow3[s]-S-1 ];
    for(int i=1;i<=m;i++)
        ans += c[i];
}

然后华丽丽地WA掉
WA
我只好上udebug的网站去拿数据,然后开始了漫漫的调试之路。

错误一

我发现数据里面有一些居然是inf!
就顺藤摸瓜,发现我的枚举顺序是由0到(目标状态-初始状态)的。

for(int S1=0;S1for(int i=1;i<=n;i++)
            d[K(S1,V[i])] = min(d[S1] + a[i],d[K(S1,V[i])]);

而我的K(增加)函数却会向上取

inline int K(int x,int y)
{
    int res=0;
    for(int i=0;i<10;i++)
        res += Pow3[i]*min(2,x%Pow3[i+1]/Pow3[i] + y%Pow3[i+1]/Pow3[i]);
    return res;
}

意思是说,假如应该达到的状态是0 1 0 0 1
最优解上的某一集合是0 1 0 1 1
加入后就“越过”了这个状态。
(因为本题只要求至少2个老师,可以超出)
这样的话就会发现我找不到这个解,于是就出错了。
改了改,写成这样:

inline int K(int x,int y)
{
    int res=0;
    for(int i=0;i<10;i++)
        res += Pow3[i]*min(2,x/Pow3[i]%3 + y/Pow3[i]%3);
    return res;
}

void Solve()
{
    int S=0;
    for(int i=1;i<=s;i++)
        S += Pow3[i-1]*min(2,st[i]);

    d[S] = 0;
    for(int S1=S;S1for(int i=1;i<=n;i++)
            d[K(S1,V[i])] = min(d[S1] + a[i],d[K(S1,V[i])]);

    ans = d[ Pow3[s]-1 ];
    for(int i=1;i<=m;i++)
        ans += c[i];
}

似乎没有问题了。。。

但是在udebug上还是有一些数据过不了,我只好暂时停一下了,叫同学看看。
但是请了好几个大神,都说无能为力

逆天之行必遭天谴

毕竟弄了一上午了,还是先暂时放一放吧。

错误二

下午老师给我们回顾了一下背包问题,(感谢背包)
再次开始调试这道题的时候,已经是晚上了。
晚上的气氛比较沉闷,正好是调试代码的好时候。

用udebug的数据弄了1个多小时,我竟然突发奇想,想到下午的背包问题,
用几个自造数据试了试,终于找到一个错误了。

咳咳(正文开始)
相信大家学过背包的,一定都知道01背包如果要用滚动数组的话,就必须要从大到小枚举。
而这道题就类似于01背包,可以举一个数据:

状态 集合
目标状态 2 2 2 2 2
中间状态 0 0 0 0 0
某一元素 1 1 1 1 1

第一次加入后:

状态 集合
目标状态 2 2 2 2 2
中间状态 1 1 1 1 1
某一元素 1 1 1 1 1

如果再加一次:

状态 集合
目标状态 2 2 2 2 2
中间状态 2 2 2 2 2
某一元素 1 1 1 1 1

这个元素就用了两次。

改了一下,就像这样:

inline int K(int x,int y)
{
    int res=0;
    for(int i=0;i<10;i++)
        res += Pow3[i]*min(2,x/Pow3[i]%3 + y/Pow3[i]%3);
    return res;
}
void Solve()
{
    int S=0;
    for(int i=1;i<=s;i++)
        S += Pow3[i-1]*min(2,st[i]);

    d[S] = 0;
    for(int i=1;i<=n;i++)
        for(int S1=Pow3[s]-1;S1>=S;S1--)
            if(d[S1] != inf)
                d[K(S1,V[i])] = min(d[S1] + a[i],d[K(S1,V[i])]);

    ans = d[ Pow3[s]-1 ];
    for(int i=1;i<=m;i++)
        ans += c[i];
}

然后就过掉了。。。
AC

反思

我想,我不会不明不白地过一道题,要被安排过得明明白白。
为什么我们一般的二进制状压dp不用倒着枚举?
留给读者思考。。(诶~你们下手轻点,还是偷偷留一个~)

而这道题给我最大的收获,也许远远不止这一点经验,它告诉我,即使再难的题,也怕我们不懈的攻关精神
在别人将就地抄书时,还有几个人在苦苦地调试自己的想法,在默默中提升自己。

完整代码

#include
#include
#include
#include
#include
#include
using namespace std;
bool pp;
int geti()
{
    int x,f=1;char c; pp=0;
    while((c=getchar())<'0' || c>'9')
    {
        if(c == '-') f=-1;
        if(c == -1) return -1;
    }
    x = c-'0';
    while((c=getchar())>='0' && c<='9') x=(x<<3)+(x<<1)+c-'0';
    if(c == '\n') pp=1;
    return x*f;
}

const int M=20,N=100,inf=0x3f3f3f3f;

int Pow3[12];
int s,m,n,ans;
int c[M+5],a[N+5];
int st[12],V[N+5];
int d[7000];

void Prepare()
{
    memset(d,0x3f,sizeof(d));
    Pow3[0]=1;
    for(int i=1;i<=10;i++)
    {
        Pow3[i] = Pow3[i-1]*3;
        st[i] = 0;
    }
    for(int i=1;i<=N;i++) V[i] = 0;

    int k;
    m=geti(); n=geti();
    for(int i=1;i<=m;i++)
    {
        c[i]=geti();
        while( (k=geti()) )
        {
            st[k]++;
            if(pp) break;
        }
    }
    for(int i=1;i<=n;i++)
    {
        a[i]=geti();
        while( (k=geti()) )
        {
            V[i] += Pow3[k-1];
            if(pp) break;
        }
    }

    for(int i=1;i<=s;i++)
        st[i] = min(st[i],2);
}

inline int K(int x,int y)
{
    int res=0;
    for(int i=0;i<10;i++)
        res += Pow3[i]*min(2,x/Pow3[i]%3 + y/Pow3[i]%3);
    return res;
}
void Solve()
{
    int S=0;
    for(int i=1;i<=s;i++)
        S += Pow3[i-1]*st[i];

    d[S] = 0;
    for(int i=1;i<=n;i++)
        for(int S1=Pow3[s]-1;S1>=S;S1--)
            if(d[S1] != inf)
                d[K(S1,V[i])] = min(d[S1] + a[i],d[K(S1,V[i])]);

    ans = d[ Pow3[s]-1 ];
    for(int i=1;i<=m;i++)
        ans += c[i];
}

int main()
{
    while( (s=geti()) )
    {
        Prepare();
        Solve();
        printf("%d\n",ans);
    }
    return 0;
}

你可能感兴趣的:(DP)