亲手写掉的第一道最小表示法!哈哈哈太开心啦~
不同于以往的几个插头(dp),这个题目的轮廓线是周围的一圈(n)个格子。而其所谓“插头”也变成了相邻格子的所属连通分量编号,并不是直接把前面几个题的思想往上套就可以轻松解决的了。这里我们就要采用一种叫最小表示法的东西来表示它的连通性信息啦~
(其实感觉是不是称之为逐格递推的轮廓线(dp)比较好。。。
而最小表示法是什么呢?举个例子,现在有这样一个序列((5,5,3,2,4,1,3,2)),序列中的每一个数代表第(i)个格子所属的连通分量。因为编号是人为设置的,所以其每一个编号也可以一一映射成等效的其他编号。为了连通性表示的规范和不重不漏,我们就把这个序列整理成意义等效的前提下字典序最小的形式,对这个序列就是((1,1,2,3,4,5,2,3))。
整理的过程很简单,既然让字典序最小,那就尽可能让前面的比后面的小。从前向后遍历,如果当前这个编号第一次出现,就把它记录下来,换成新的编号(tot+1)。否则就直接将其改为已经记录下来的对应编号。
void min_express (int &zt) {
// zt 表示状态,tot 表示新整理的最大编号。
int tot = 0, id[N] = {0};
for (int i = 0; i < n; ++i) {
int now = get_wei (zt, i);
if (!now) continue;
if (id[now] != 0) {
zt = alt_wei (zt, i, id[now]);
} else {
zt = alt_wei (zt, i, id[now] = ++tot);
}
}
}
改完以后就可以考虑状态的转移了。这一点比较好考虑,都是插头(dp)的套路了。状态转移的讲解摘自洛谷日报——插头(dp)。(因为我太懒了(QwQ))
- 不选
当然得考虑状态合理,无下插头或下插头所在联通块还有其他插头,否则下插头被孤立而不形成联通块了
大家发现没有?
这里每个状态仅合理而已,并不能确定这是可取的最终状态,因为最后得保证只有一个联通块(其实上文也),详细请看 $update$
那为什么不考虑右插头呢?而要判断下插头呢?
因为下插头以后再也不会判断了,所以要考虑状态合理,右插头到下一行自然会考虑状态
右插头来自上一个转移状态,很可能形成新块 $7$ ,故这样转移条件会变得严苛,最终答案通常会小于正确答案
- 选
(1) !b1 and !b2 单独形成新块,此块命名为7
(2) b1 or b2 此块与插头相连,更新联通块状态,这就是伟大的最小表示法
一些解释:
-
(Q):为什么把新的连通块设置为7?
- (A):在最小表示法下,连通块标号从小到大记录。为了防止冲突,我们随手采用一个可以采用的里面最大的,到后面还会进行整理。
-
(Q):连上其他的分量应该怎么操作?
- (A):直接改就完事了(=\_=)。比如这个题里面,只要(b1)和(b2)有一个选中,你就可以连上其他分量,为了省一种讨论,我对目前这个格子采用了取两个分量较大值的方法,然后把其他编号等于两个分量编号较小值且不为(0)的块也改成这个较大分量的编号。
(Code):
#include <bits/stdc++.h>
using namespace std;
const int N = 9 + 5;
const int INF = 2e9;
const int base = 599999;
const int M = 600000 + 5;
void cmax (int &x, int y) {x = max (x, y);}
int ans = -INF;
int n, w[N][N];
int cur, las, cnt[2];
int nxt[M], head[M], dp[2][M], Hash[2][M];
int get_wei (int zt, int wei) {
return (zt >> (wei * 3)) % 8; // 8 进制状压
}
int alt_wei (int zt, int wei, int val) {
return zt - ((get_wei (zt, wei) - val) << (wei * 3));
}
int count (int zt, int val) {
int ret = -1;
for (int i = 0; i < n; ++i) {
ret += (get_wei (zt, i) == val);
}
return ret;
}
void min_express (int &zt) {
int tot = 0, id[N] = {0};
for (int i = 0; i < n; ++i) {
int now = get_wei (zt, i);
if (!now) continue;
if (id[now] != 0) {
zt = alt_wei (zt, i, id[now]);
} else {
zt = alt_wei (zt, i, id[now] = ++tot);
}
}
}
bool can_use (int zt) {
int tot0 = 0, tot1 = 0;
for (int i = 0; i < n; ++i) {
tot0 += get_wei (zt, i) == 0;
tot1 += get_wei (zt, i) == 1;
if (get_wei (zt, i) > 1) {
return false;
}
}
if (!tot1) return false;
return true;
}
void update (int zt, int val) {
min_express (zt);
if (can_use (zt)) {
ans = max (ans, val);
}
int _zt = zt % base;
for (int i = head[_zt]; i; i = nxt[i]) {
if (Hash[cur][i] == zt) {
cmax (dp[cur][i], val); return;
}
}
nxt[++cnt[cur]] = head[_zt];
head[_zt] = cnt[cur];
Hash[cur][cnt[cur]] = zt;
dp[cur][cnt[cur]] = val;
}
void print (int x, int y) {
cout << "r = " << x << " c = " << y << endl;
for (int i = 1; i <= cnt[cur]; ++i) {
cout << "zt = " << Hash[cur][i] << " ";
for (int j = 0; j < n; ++j) {
if (j != 0) cout << "_";
cout << get_wei (Hash[cur][i], j);
}
cout << " val = " << dp[cur][i] << endl;
}
}
int solve () {
update (0, 0);
// print (0, 0);
for (int i = 1; i <= n; ++i) {
for (int j = 1; j <= n; ++j) {
las = cur, cur ^= 1, cnt[cur] = 0;
memset (head, 0, sizeof (head));
for (int k = 1; k <= cnt[las]; ++k) {
int zt = Hash[las][k];
int b1 = (j >= 2 ? get_wei (zt, j - 2) : 0);
int b2 = (j >= 1 ? get_wei (zt, j - 1) : 0);
int val = dp[las][k];
// 1. 不选当前格
if (!b2 || count (zt, b2)) {
// 没有插头 b2 或者 插头 b2 与轮廓线上其他的格子连通
update (alt_wei (zt, j - 1, 0), val); // 当前格子置为不选
}
if (!b1 && !b2) { // 新建连通分量
update (alt_wei (zt, j - 1, 7), val + w[i][j]);
}
if (b1 || b2) { // 连上其他的分量
int id = max (b1, b2), _zt = zt;
_zt = alt_wei (_zt, j - 1, id);
for (int i = 0; i < n; ++i) {
if (get_wei (_zt, i) == 0) continue;
if (get_wei (_zt, i) == min (b1, b2)) {
_zt = alt_wei (_zt, i, id);
}
}
update (_zt, val + w[i][j]);
}
}
// print (i, j);
}
}
return ans;
}
int main () {
// freopen ("data.in", "r", stdin);
cin >> n;
for (int i = 1; i <= n; ++i) {
for (int j = 1; j <= n; ++j) {
cin >> w[i][j];
}
}
cout << solve () << endl;
}