LG4003 无限之环 题解
Tag
状态压缩,动态规划。
Preface
碎碎念:
考场上看到这个题的一瞬间想到了要费用流,但是不会建图。
刚刚准备自闭的时候突然发现好像可以状态压缩。
状压打完了发现这玩意状态好少啊……
果然拿个 map 存一下就过了……
Description
给定一个 \(n \times m\) 的网格图,有 \(15\) 种形状的水管排布在里面,需要这些水管互相相连,每次可以转动 \(90^\circ\),求最少的操作次数。
\(\texttt{data range}: n\times m \leq 2\times 10^3\).
Solution
对于本题的费用流解法笔者不想在这里赘述,这个地方讨论的是本题的状态压缩解法。
我们很容易想出来一个 \(O(nm2^{\min(n,m)})\) 的部分分做法,也就是本题 40pts 的部分分,也就是用轮廓线 dp 来转移状态,可以设一个 01 状态为一个棋盘的轮廓线形状,令 \(2^i\) 为第 \(i\) 个位置是否有向下插的管子,\(2^0\) 为有没有向右插的管子,然后直接转移就可以做出部分分了。
但是笔者在考场上写不出费用流的情况下写了一个状态压缩,然后仔细一想……
初始状态一个,转移出来的状态难道会很多吗?
于是不严谨的估计了一下我们题目所能够转移出来的状态数量,我们粗略的考虑每一种情况会转移出多少种状态以及其可以接受多少种合法的状态。
不难发现一个格子只能接受最多四种与其直接相关的状态,也就是其上面有没有管子,其左边有没有管子这四种状态,我们用 \(0/1,0/1\) 来表示这些状态。
再对于所有的情况分类讨论,
- 一个接口的统一讨论。不难发现其可以接受 \(01,10,00\) 这 \(3\) 种状态,对于前两种状态,其只能导出一个状态。对于最后一个状态可以导出 \(2\) 种状态。所以这个情况可以是状态增加点,也可以是状态减少点。
- 对于一个杠杠的分别只能接受一种状态和导出一种状态,所以一定是状态减少点。
- 对于一个弯道的情况统一讨论,不难发现其可以接受所有的状态,但是对于每种状态只能导出一种状态,所以是状态不变点,只会对答案造成影响。
- 对于一个三个道的情况统一讨论,不难发现其可以接受 \(10,01,11\) 这三种状态。相同的,对于前两种状态只能导出一种状态,最后一个可以导出两种状态,所以是即是状态增加点,也是状态减少点。
- 对于十字路口,其只能接受一种状态并且导出一种状态,所以一定是状态减少点。
最后我们发现了,只有 \(1\) 情况和 \(4\) 情况会增大状态。
我们大胆猜测,他的数据很扯淡,所以你用个 map 存一下状态然后就过了……还跑的飞快。
实际上我们想要卡这个做法也非常简单,只用第一行全都是第一种状态就可以把这个做法卡到上界,但是实际上估计出题人实在是没有想到还有这种人类智慧做法,所以压根没卡。
事实上 UOJ 上面有数据 hack 了这种做法,还是希望大家练习的时候多学正解考试的时候多写乱搞,这样就可以提高自己的实力了!
Code
给出我乱搞的代码。
using ll = long long;
const int INF = 1e9;
const int N = 2e3 + 10 ;
int n, m;
int a[N][N];
bool up(const int x) {return x & 1;}
bool ri(const int x) {return (x >> 1) & 1;}
bool dn(const int x) {return (x >> 2) & 1;}
bool le(const int x) {return (x >> 3) & 1;}//上下左右
int l90(const int x) {return (x >> 1) | ((x & 1) << 3);}
int r90(const int x) {return ((x << 1) & 15) | ((x >> 3) & 1);}
int t18(const int x) {
return (dn(x) << 0) | (le(x) << 1) | (up(x) << 2) | (ri(x) << 3);
}//旋转
bool sps(const int x) { return x == 5 || x == 10; }//直线型特判
int b[N][N];
void rotate() {
FOR(i, 1, n) FOR(j, 1, m) b[m - j + 1][i] = l90(a[i][j]);
swap(n, m);
memset(a, 0, sizeof(a));
FOR(i, 1, n) FOR(j, 1, m) a[i][j] = b[i][j];
return ;
}
inline void input() {
n = rd, m = rd;
FOR(i, 1, n) FOR(j, 1, m) a[i][j] = rd;
return ;
}
inline ll To(ll x, ll S, int pos) {
if(((S >> pos) & 1) ^ up(x)) return -1;
if((S & 1) ^ le(x)) return -1;
if(pos == m && ri(x)) return -1;
ll ret = (((ll) 1ll << pos) ^ -1ll) & S;
ret = ret & (1ll ^ -1ll);
return ret | ((ll)dn(x) << pos) | ri(x);
}
unordered_map<ll, int> f, g;
inline void work() {
if(n < m) rotate();
f[0] = 0;
FOR(i, 1, n) FOR(j, 1, m) {
for(auto x : f) {
if(x.second == INF) continue;
int w = a[i][j];
ll S = x.first, tem;
tem = To(w, S, j);
if(~tem) {
auto it = g.insert(make_pair(tem, INF));
cmin(it.first->second, x.second);
}
if(sps(w)) continue;
tem = To(l90(w), S, j);
if(~tem) {
auto it = g.insert(make_pair(tem, INF));
cmin(it.first->second, x.second + 1);
}
tem = To(r90(w), S, j);
if(~tem) {
auto it = g.insert(make_pair(tem, INF));
cmin(it.first->second, x.second + 1);
}
tem = To(t18(w), S, j);
if(~tem) {
auto it = g.insert(make_pair(tem, INF));
cmin(it.first->second, x.second + 2);
}
}
f.clear(), f.swap(g);
}
auto it = f.insert(make_pair(0, INF));
if(it.second) cout << -1 << '\n';
else cout << it.first->second << '\n';
return ;
}
inline void solve() {
input();
work();
return ;
}
Final
- 出题人:如果直线型水管可以旋转,如何建图?
- KAMIYA:你不让我动直线型我不是又要写一个函数来特判,
淦你娘。