[20210404]密室逃脱
壹、题目描述 ¶
有 \(n\) 个房间,编号为 \(1\) 到 \(n\).
有 \(n-1\) 个隧道,第 \(i\) 个隧道连接房间 \(i\) 和 \(i+1\). 隧道在正常情况下是关闭着的,要打开第 \(i\) 个隧道需要有 \(a_i\) 个人在房间 \(i\) 按住开关或者 \(b_i\) 个人在房间 \(i+1\) 按住开关。按开关的人不能进行任何其它操作(比如移动或者同时按另一个开关),
一旦他们松开开关,隧道会立刻关上。
在房间 \(1\) 有一个隧道通往出口,要打开这个隧道需要 \(m\) 个人按住开关。你想知道在保证这个隧道在任何时刻都不会被打开的情况下,最多可以有多少个人(你可以指定他们初始在哪个房间)。
对于全部数据,满足 \(1\le n\le 1000,1\le m,a_i,b_i\le 10000\).
贰、题解 ¶
一道很生动的题,因为有这样一种情况:
一堆人,先派 \(a_i\) 个人按住左边,等剩下的人都跑到右边去了,满足 \(b_i\) 之后再让本来在门左边的人过来。
毕竟人都是可以团队合作的嘛......
考虑上述情况发生在什么时候:当一行 \(p\) 个人从左边过来,首先需要满足 \(p\ge a_i\),同时,剩下的人还得去右边按门,所以还有 \(p-a_i\ge b_i\Leftrightarrow p\ge a_i+b_i\),也就是说如果我们有 \(p(p\ge \max\{a_i+b_i\})\),那么我们哪里都可以去了。
接下来,考虑 \(\tt DP\),状态如何定义?首先肯定得有一个记录第几道门的 \(n\) 大小,另外,发现走到当前房间的人数也有关系,考虑将这一维放进状态,所以我们有了状态数组:
初始化有 \(f_{1,j}=j(0\le j<m)\).
考虑转移,对于一道从左到右的门,有 \(\lang a_i,b_i\rang\),考虑一行 \(j\) 个人遇到的情况:
- 当 \(j<a_i\),连这道门都打不开了......这个时候,我们可以考虑在第 \(i+1\) 个房间放 \(b_i\) 个人为他们开门,也可以放 \(<b_i\) 个人让他们隔开,所以这个状态可以向 \(f_{i+1,j+b_i}\) 或者 \(f_{i+1,0\sim b_i-1}\) 转移;
- 当 \(a_i\le j<a_i+b_i\),必定有一些人来按住这道门,所以会消耗掉 \(a_i\) 个人,这个状态会向 \(f_{i+1,j-a_i}\) 转移;
- 当 \(j\ge a_i+b_i\),这道门对他们已经没什么用了,可以考虑直接向 \(f_{i+1,j}\) 转移;
直接转移就行了,但是考虑第一种情况,我们只需要选择最大的 \(f_{i,j}(j<a_i)\) 进行转移即可。
FAQ
Q: 第二种情况为什么不用考虑在右边放 \(b_i\) 个人来进行转移呢?
A: 其一,转移到的就不是 \(\lang i+1,j-a_i\rang\) 了,其二,这个状态会被第三种转移包含。
Q: 为什么我们只需要考虑从左边到右边的转移而不是从右边进行转移呢?
A: 因为实际上我们第一种转移就是变相地从右边往左边走,而第二种是从左边往右边走,第三种是左右皆可走的情况。
Q: 这个 \(f\) 数组的第二维开多大比较好?
A: \(20000\) 即可,如果有房间的人有那么大已经可以保证所有人可以通过所有门了。
下面贴一下题解思路:
注意到人的操作是可逆的,那么我们考虑将所有可能出现的状态整合成一些“标准状态”。我们不停合并能全部走到一起的人,那么会形成这样的情况:
有若干个段,每段之间是不连通的;
每段中有若干个小段,每小段中至少会有若干人,其余的人则可以在整个段中任意移动。
那么我们发现能到达某个房间的人数是固定的(与其它人在哪里无关)。
叁、参考代码 ¶
#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
inline void getmax(int& x, int y){ x=(x>y? x: y); }
template<class T>inline T readin(T x){
x=0; int f=0; char c;
while((c=getchar())<'0' || '9'<c) if(c=='-') f=1;
for(x=(c^48); '0'<=(c=getchar()) && c<='9'; x=(x<<1)+(x<<3)+(c^48));
return f? -x: x;
}
const int maxn=1000;
const int maxm=20000;
int a[maxn+5], b[maxn+5];
int n, m;
inline void input(){
n=readin(1), m=readin(1);
for(int i=1; i<n; ++i) a[i]=readin(1), b[i]=readin(1);
}
int f[maxn+5][maxm+5];
inline void getf(){
memset(f, -0x3f, sizeof f);
for(int j=0; j<m; ++j) f[1][j]=j;
int ans=0;
for(int i=1; i<=n; ++i){
int maxx=-0x3f3f3f3f;
for(int j=0; j<=maxm; ++j){
if(j<a[i]){
getmax(f[i+1][j+b[i]], f[i][j]+b[i]);
getmax(maxx, f[i][j]);
}
else if(j<a[i]+b[i])
getmax(f[i+1][j-a[i]], f[i][j]);
else getmax(f[i+1][j], f[i][j]);
getmax(ans, f[i][j]);
}
for(int j=0; j<b[i]; ++j)
getmax(f[i+1][j], maxx+j);
}
printf("%d\n", ans);
}
signed main(){
freopen("escape.in", "r", stdin);
freopen("escape.out", "w", stdout);
input();
getf();
return 0;
}
肆、用到 の \(\tt trick\) ¶
写 \(\tt DP\) 的时候,先将题目特性分析清楚再写状态,有些特性能够帮助我们写出状转。
另外,这个状转也很妙啊,看似是从左往右做的 \(\tt DP\),但是转移将从右往左的情况也考虑了。