洛谷2157
看到\(b\)的范围只有\(7\),可以往搜索和状压方向想
因为搜索出现频率太低,先考虑状压
显然应该将\(b\)的意义设为状压集合
所以设\(f[i][j][k]\)表示现在打完饭的人是\([1,i-1]\)以及\(j\)所表示的集合(这里\(j\)所表示的集合不太好解释,举个例子,如\(j\)在二进制表示下为\(1010\),则表示\(i+1\),\(i+3\)已经打完了饭;若为\(1011\),则表示\(i\),\(i+1\),\(i+3\)已经打完了饭),最后一个打饭的人是\(i+k\)(\(-8\le k\le7\))所用时间最小值(显然\(k\)这一维需要偏移数组,但下文为了方便,就暂不偏离)
注意这里的状态不能设置为先打完饭的人是\([1,i-1]\),然后再是\(j\)所表示的集合打饭。否则下面的数据会出错
1
9
9 4
5 7
9 3
8 1
6 6
3 5
4 5
9 4
3 0
正确答案应该是\(19\),一种正确顺序:\(4\) \(1\) \(3\) \(8\) \(6\) \(9\) \(5\) \(7\) \(2\)
首先判断当前状态是否合法
如果存在某一个没打饭的人,后面有一个人已经打了饭且这个人的位置比这个没打饭的人的忍耐度还大,那就不合法(这个过程小模拟即可,不是本文重点,故不再赘述)
在当前状态合法之后,分情况DP
- \(k\le-1\)
即此时最后一个打饭的人是在\(i\)之前,所以\(f[i][j][k]=min(f[i+k][(j<<-k)|((1<<-k)-1)][0])\)
这个方程是什么意思呢?最后一个打饭的人是\(i+k\),从\(i+k+1\)一直到\(i-1\)都已经打好了饭,再加上\(j\)集合里的人也已经打好了饭,所以总的已经打好了饭的人的集合就是\((j<<-k)|((1<<-k)-1)\),而且第三维为\(0\)(因为最后一次打的饭的人刚好就为\(i+k\))
- \(0\le k\le7\)
那我们就去掉\(j\)里面的第\(k\)个人即可,有新集合\(o=j\)^\((1<<k)\)
此时在讨论在新集合状态下,最后一个打饭的人是谁,如果仍然是\(i\)前面的人,那么跟第一条类似,您可以自己推一下
如果是在\(i\)及之后,那么定下一个人转移即可
\(code\)
#include<bits/stdc++.h>
using namespace std;
#define ll long long
const int N=1010;
int n;
int t[N],b[N];
int f[N][1<<8][16];
int read()
{
int x=0,f=1;char s=getchar();
while(s<'0'||s>'9'){if(s=='-')f=-f;s=getchar();}
while(s>='0'&&s<='9'){x=x*10+s-48;s=getchar();}
return x*f;
}
bool valid(int i,int j)
{
int temp=j;
for(int k=0;k<=7&&i+k<=n;k++)
{
if((j>>b[i+k]+1)&&!(temp&(1<<k))) return 0;
j>>=1;
}
return 1;
}
int main()
{
int c=read();
while(c--)
{
n=read();
for(int i=1;i<=n;i++)
t[i]=read(),b[i]=read();
memset(f,0x3f,sizeof(f));
for(int i=0;i<=7;i++)
if(valid(1,1<<i))
f[1][1<<i][i+8]=0;//初始化
for(int i=1;i<=n;i++)
{
for(int j=0;j<(1<<8);j++)
{
if(!valid(i,j)) continue;
for(int k=-8;k<=-1;k++)
{
if(i+k<=0) continue;
if(i+k+b[i+k]<i-1) continue;
if((j>>b[i+k]+k+1)) continue;
if((j<<-k)>=(1<<8)) continue;
f[i][j][k+8]=min(f[i][j][k+8],f[i+k][(j<<-k)|((1<<-k)-1)][8]);
}
for(int k=0;k<=7;k++)
{
if(!(j&(1<<k))) continue;
int o=(j^(1<<k));
for(int l=-8;l<=-1;l++)
{
if(i+l<=0) continue;
if(i+l+b[i+l]<i-1) continue;
if((o>>b[i+l]+l+1)) continue;
if((o<<-l)>=(1<<8)) continue;//这里一堆都是合法性判断,您可以尝试理解
f[i][j][k+8]=min(f[i][j][k+8],f[i+l][(o<<-l)|((1<<-l)-1)][8]+(t[i+k]|t[i+l])-(t[i+k]&t[i+l]));
}
for(int l=0;l<=7;l++)
{
if(!(o&(1<<l))) continue;
f[i][j][k+8]=min(f[i][j][k+8],f[i][o][l+8]+(t[i+k]|t[i+l])-(t[i+k]&t[i+l]));
}
}
}
}
int ans=0x7fffffff;
for(int i=0;i<=min(7,n);i++)
for(int j=0;j<=i;j++)
ans=min(ans,f[n-i][(1<<i+1)-1][j+8]);
printf("%d\n",ans);
}
return 0;
}
后记和感想
这题其实我做了\(12\)个小时,最开始做的时候是在\(6\)个月前,当时一直磕这道题,花了\(8\)小时,最后得到了\(0\)分的好成绩。当时我都已经想哭了,想着自己为什么这么蠢,也想着这么多时间花了也没什么用(当时连题解也没有看懂)。后来我一直都很怕这道题,看见他就只想逃避(一朝被蛇咬十年怕井绳?),最后今天(2021-9-29)鼓起勇气重新做,花了\(4\)个小时(包括多次更改状态,边界条件代码细节等等)最终才A了。看着绿色的AC,只能说心情真的很好。也许这就是OI的魅力吧,为了只是最后一刻的盛开。很可惜我马上就要退役了,事实证明了我确实没啥天赋,也许不能再体会这种令人面红耳赤的感觉了。假如您再看这篇题解,如果您有天赋和资质,那么恭喜您,未来的金牌在等着您!如果您跟我一样只是一个天赋平平的普通人,那么没关系,人各有志,普通人也有普通人的活法。不管您是谁,祝您幸福。