自从开始复习DP,我就天天抄题解
第一眼看到题目:我是谁?我在哪?我要干什么?
注意到$B$的范围十分友好,明显要状压。我要状压啥?
于是愉悦地抄起了题解
$f(i,st,k)$表示前$i-1$个人已经吃完,第$i$个人到第$i+7$个人的状态为$st$($0$为没吃,$1$为吃了)最短时间。因为做一道菜的时间和上一道有关系,所以用$k$表示上一个吃的人为第$i+k$个人$(k\in [-8,7])$。
Q:为啥k的意义这么奇怪?不能直接用k表示上一个吃的人为k?
A:可以发现上一个吃的人范围一定在$[i-8,i+7]$内,超出这个范围这个状态就是不合法的,就能直接省掉空间与时间。
初始状态:$f(1,0,-1)=0$(没有人吃饭,第$1$个人到第$8$个人都没吃饭,上一道菜为第$0$道,即还没有做菜)
考虑转移:
- 如果$st$第一位为$1$(第$i$个人吃了),那就直接向$f(i+1,st>>1,k-1)$转移。这两个状态其实是一样的,不用枚举下一个谁吃饭。
- 如果$st$第一位为$0$,枚举下一个谁吃了,转移到$f(i,st|(1<<j),j)$,还要注意满足每个人的容忍度,记录当前最大能吃饭的人的范围,一但超出就停止。
答案即为$\min \{f(n+1,0, -8)\sim f(n+1,0,0)\}$
Q:为什么不考虑不合法的状态呢?
比如说$st$中第3个人没吃,第6个人吃了,而第3个人容忍度为2,这样转移不就错了吗?
A:转移时都是向合法的状态转移,不合法的都被过滤掉了。所以当遇到不合法状态时,它从未被转移过,值也就为$inf$。
代码:
#include <iostream>
#include <cstdio>
#include <algorithm>
#include <cmath>
#include <cstring>
#define maxn 1005
#define inf 0x3f3f3f3f
using namespace std;
inline int read(){
int x=0,y=0;
char ch=getchar();
while(ch<'0'||ch>'9'){if(ch=='-')y=1;ch=getchar();}
while(ch>='0'&&ch<='9')x=(x<<1)+(x<<3)+(ch^48),ch=getchar();
return y?-x:x;
}
int t[maxn],b[maxn],F[maxn][1<<9][20],*f[maxn][1<<9];
inline int calc(int i,int j){
if(!i)return 0;
return t[i]^t[j];
}
//t[i]|t[j]-t[i]&t[j]就相当于t[i]^t[j]
int main(){
int c=read();
for(register int i=0;i<maxn;++i)
for(register int j=0;j<(1<<9);++j)
f[i][j]=&F[i][j][10];
//小技巧:用指针处理可以使数组访问负数下表
while(c--){
int n=read();
for(register int i=1;i<=n;++i)
t[i]=read(),b[i]=read();
memset(F,0x3f,sizeof F);
f[1][0][-1]=0;
for(register int i=1;i<=n;++i){
for(register int st=0;st<(1<<8);++st)
for(register int k=-8;k<=7;++k){
if(f[i][st][k]==inf)continue;
if(1&st)
f[i+1][st>>1][k-1]=min(f[i+1][st>>1][k-1],f[i][st][k]);
else {
for(register int j=0,li=7;j<=li;++j)//li是最大容忍范围
if(!((1<<j)&st)){
f[i][st|(1<<j)][j]=min(f[i][st|(1<<j)][j],f[i][st][k]+calc(i+k,i+j));
li=min(li,j+b[i+j]);
}
}
}
}
int ans=inf;
for(register int i=-8;i<=0;++i)
ans=min(ans,f[n+1][0][i]);
printf("%d\n",ans);
}
}