[bzoj 1226] [SDOI2009]学校食堂Dining:状态压缩的奥妙

题意:N个人(N<=1000)排队吃饭,第i个人允许第i+1~i+b[i]个人先吃(0<=b[i]<=7),每个人有口味t[i],做每个人的菜的时间是他的口味和所做的上一个人的菜的口味的函数,不计算第一个人的时间,求最小总时间。

没能自己做出来。进行了如下尝试:
1. 设f[i][S]为前(i-1)个人吃了,第i个人没吃,后面7个人状态为S。没有记录最后一个人是谁,无从转移。
2. 设f[i][j][S]为前(i-1)个人吃了,最后一个吃饭的人是j,i后面7个人状态为S。首先空间就爆掉了。
3. 设f[i][S]为最后一个吃饭的是i,他后面的人都没吃,前面7个人状态为S。许多状态无法表示。
4. 设f[i][S1][S2]为最后一个吃饭的是i,前面7个人状态为S1,后面7个人状态为S2。空间没爆,理论上会超时。

和正解接近,但都差一点。把2号思路改进一下。i和j不会相差很大。事实上是-8~7。所以记录这个差值就好了……

状态压缩,不能局限于把状态压到一个数里,更要充分发掘题目的性质,选择最合适的表示方法以降低时空复杂度。以本题为例,由于b[i]<=7,在任何一个人吃饭之前,他后面的第7个以后不可能已经吃过。

为什么正解总是会偷偷溜走呢?

关于实现……我的代码不优美。参考了一下神犇们的写法,总结出四点:
1. 差值可以是正的,也可以是负的。Pascal处理这个很方便,但是C++……可以定义一个宏,完成映射。
2. 我定义的状态是f[i][j][S]: 前(i-1)个人已用餐,第i个人未用餐,第i+1~i+7个人状态为S,最后一个用餐的是第(i+j)个同学(这里的叙述中j未映射成非负数)。这样其实浪费了差值为0的那一维,而且为了符合定义,第i个人用餐后必须找到下一个未用餐的同学进行转移。更简洁的做法是,定义状态为:前(i-1)个人已用餐,第i个人未用餐,第i~i+7个人状态为S;当第i个人已用餐,转移至f[i+1][j-1][S>>1],当第i个人未用餐,进行枚举。感觉我的定义正确性更显然 QAQ 正是因为第i个人未用餐,第(i+7)个以后的才一定没有用餐,所以可以不记录。去掉这一重限制,多出来的状态可以看作第i个人用餐后产生的中间量。每一次往(i+1)推,和直接找下一个未用餐的人是等价的。
3. 边界可以用f[1][-1][0] = 0来处理,计算cost的时候特判一下,在适当的时候返回0。
4. 我写了个valid函数判断转移是否合法。想到了可以边枚举边计算,但是,我把边界单独处理,因此不想把相似的代码写两遍……

#include <cstdio>
#include <cstring>
#include <algorithm>
using namespace std;
const int MAX_N = 1000, MAX_B = 7, inf = 0x3f3f3f3f;
int N, t[MAX_N+1], b[MAX_N+1], f[MAX_N+2][16][1<<MAX_B];

template<typename T>
inline void tension(T& x, T v)
{
    x = min(x, v);
}

inline int cost(int a, int b)
{
    return (t[a] | t[b]) - (t[a] & t[b]);
}

bool valid(int x, int S, int k)
{
    for (int i = x+1; i < k; ++i)
        if (!(S & (1<<(i-x-1))) && i+b[i] < k)
            return false;
    return true;
}

// f[i][j][S]: 前(i-1)个人已用餐,第i个人未用餐,第i+1~i+7个人状态为S,最后一个用餐的是第i+j-8个同学
int dp()
{
    memset(f, 0x3f, sizeof(f));
    f[2][7][0] = 0;
    for (int i = 1; i <= b[1] && 1+i <= N; ++i)
        if (valid(1, 0, 1+i))
            f[1][i+8][1<<(i-1)] = 0;
    for (int i = 1; i <= N; ++i)
        for (int k = 0; k < (1<<7); ++k)
            for (int j = max(0, 9-i); j < 16; ++j) {
                if (f[i][j][k] == inf)
                    continue;
                int p = 0;
                while (k & (1<<p))
                    ++p;
                ++p;
                tension(f[i+p][8-p][k>>p], f[i][j][k] + cost(i+j-8, i));
                for (int l = i+1; l <= i+b[i] && l <= N; ++l)
                    if (!(k & (1<<(l-i-1))) && valid(i, k, l))
                        tension(f[i][l-i+8][k | (1<<(l-i-1))], f[i][j][k] + cost(i+j-8, l));
            }
    int ans = 1<<30;
    for (int j = 0; j < 16; ++j)
        if (f[N+1][j][0] < inf)
            tension(ans, f[N+1][j][0]);
    return ans;
}

int main()
{
    int C;
    scanf("%d", &C);
    while (C--) {
        scanf("%d", &N);
        for (int i = 1; i <= N; ++i)
            scanf("%d %d", &t[i], &b[i]);
        printf("%d\n", dp());
    }
    return 0;
}
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值