玉米田
UPD:
2021.7.20:修改了版面、添加了两种优化方法。
链接:
题目大意:
在一个有障碍的矩阵中放置物品,要求不能相邻,求方案数。
思路:
很容易想到是状压 DP。
朴素:
设 (f_{i,j}) 表示第 (i) 行的方案是 (j)(是一个二进制数,用十进制来存储,第 (k) 位是 (1/0)(二进制)表示选或不选)时的方案数。
声明:下面的 (j,k) 都是一个合法的方案。
若已经进行到 (i) 行,此时的方案是 (j),上一行的方案是 (k)。
有一个特殊条件(边界):(i = 1)。
既然是第一行,那么它的所以合法方案都是正确的,所以边界是:
也可以很容易地想到本行的合法方案的方案数是上一行的所有合法方案数,也就是:
朴素代码:
时间复杂度 (mathcal{O}(n{F_m}^2)),其中 (F_m) 表示宽为 (m) 的无障碍行的合法方案数。
const int N = 15;
int n, m;
int f[N][(1 << N)];
int st[1 << N];
int a[N];
int tot;
void _init() //缩小状态集合优化,判断此方案 在这一行 是不是合法的
{
for (int i = 0; i < (1 << m); i++)
{
if (i & (i << 1)) continue;
st[++tot] = i;
}
}
int main()
{
scanf ("%d%d", &n, &m);
for (int j = 1; j <= n; j++)
for (int i = m - 1; i >= 0; i--)
{
int x;
scanf ("%d",&x);
a[j] += (x << i);
}
_init();
for (int i = 1; i <= tot; i++) //边界条件
{
if (!((st[i] | a[1]) == a[1]))continue; //是否合法
f[1][st[i]] = 1;
}
for (int i = 2; i <= n; i++)
{
for (int j = 1; j <= tot; j++)
{
if (!((st[j] | a[i]) == a[i]))continue; //判断合法
for (int k = 1; k <= tot; k++)
{
if (!((st[k] | a[i - 1]) == a[i - 1]))continue; //同上条注释
if (st[j] & st[k]) continue;
f[i][st[j]] += f[i - 1][st[k]]; //转移
f[i][st[j]] %= 100000000;
}
}
}
int ans = 0;
for (int j = 1; j <= tot; j++)
ans += f[n][st[j]], ans %= 100000000;
printf ("%d", ans);
return 0;
}
轮廓线 DP:
优化是无止境的。若 (n,mleq19),要怎么解决本题呢?
可采用轮廓线 DP。设 (f_{i,j,k}) 表示当前点在 ((i,j)),轮廓线的状态是 (k) 的方案数:
? | ? | ? | ? |
---|---|---|---|
? | ? | (mathrm{st}_{1}) | (mathrm{st}_{2}) |
(mathrm{st}_{3}) | (mathrm{st}_{4}) | (x) | ? |
如表格,轮廓线的状态为 (mathrm{st}_{3}mathrm{st}_{4}mathrm{st}_{1}mathrm{st}_{2}),其中 (mathrm{st}_{n}) 是一位二进制数,表示是否选择所在格子。
那么转移的话,考虑到 (x) 的上面和左面是否是 (1),分类讨论即可。关于转移方程,位运算使得公式很难看,所以还是在代码看好了。
轮廓线 DP 代码:
时间复杂度 (mathcal{O}(nm2^m))。
const int N = 30, M = 524298;
const int mod = 100000000;
inline ll Read()
{
ll x = 0, f = 1;
char c = getchar();
while (c != '-' && (c < '0' || c > '9')) c = getchar();
if (c == '-') f = -f, c = getchar();
while (c >= '0' && c <= '9') x = (x << 3) + (x << 1) + c - '0', c = getchar();
return x * f;
}
int n, m, Now = 0;
int f[2][M];
int a[N][N];
int main()
{
n = Read(), m = Read();
for (int i = 1; i <= n; i++)
for (int j = 1; j <= m; j++)
a[i][j] = Read();
f[0][0] = 1;
for (int i = 1; i <= n; i++)
for (int j = 1; j <= m; j++)
{
Now ^= 1;
for (int k = 0; k < (1 << m); k++)
{
int lf = (j == 1? 0: (k >> (j - 2)) & 1), up = (k >> (j - 1)) & 1;
if ((i == 1 && up) || (j == 1 && lf)) continue;
if (up) f[Now][k ^ (1 << j - 1)] += f[Now ^ 1][k]; //即上面选了,这里不能选
else if (lf || !a[i][j]) f[Now][k] += f[Now ^ 1][k]; //即左边选了或有障碍,这里不能选
else // 都可以选
{
f[Now][k] += f[Now ^ 1][k];
f[Now][k ^ (1 << j - 1)] += f[Now ^ 1][k];
}
}
for (int k = 0; k < (1 << m); k++) // 卡常数,减法的常数比取模小;
{ //顺便实现memset(f[Now ^ 1], 0, sizeof f[Now ^ 1]) 也是卡常
for (; f[Now][k] > mod; f[Now][k] -= mod);
f[Now ^ 1][k] = 0;
}
}
int ans = 0;
for (int k = 0; k < (1 << m); k++)
ans = (ans + f[Now][k]) % mod;
printf ("%d", ans);
return 0;
}
轮廓线且缩小状态集合:
优化是无止境的。若 (nleq120,mleq21) 呢?
在朴素中,我们用了缩小状态集合优化,使得时间复杂度从原本的 (mathcal{O}(n4^m)) 降到 (mathcal{O}(n{F_m}^2))。
为了让时间复杂度更小,我们也考虑让轮廓线 DP 也缩小状态集合。但似乎遇到一个瓶颈:朴素的状态是一整行,所以可以直接判断相邻确定合法;而轮廓线的状态可能分布在两行,就是说出现一组相邻都选的情况是合法的。难道不能确定合法吗?
跳出思维,越过瓶颈。它存在相邻的地方就是行的分割处,把它记录下来,在 DP 时根据当前点的位置就单纯枚举“当前是分割点”的状态即可。
轮廓线且缩小状态集合代码:
时间复杂度不会算。但实测比神仙 cmd 的代码快。
const int N = 121, N2 = 22, M = 1747628, M2 = 5e4;
const int mod = 100000000;
inline ll Read()
{
ll x = 0, f = 1;
char c = getchar();
while (c != '-' && (c < '0' || c > '9')) c = getchar();
if (c == '-') f = -f, c = getchar();
while (c >= '0' && c <= '9') x = (x << 3) + (x << 1) + c - '0', c = getchar();
return x * f;
}
int n, m, Now = 0;
int f[2][M2];
int a[N], st[N2][M2];
short id[2][M]; // 用 short 压空间
int main()
{
n = Read(), m = Read();
for (int i = 0; i < (1 << m); i++)
{
int u = i & (i >> 1);
if(u != (u & -u)) continue;
int p = u? log2(u) + 1: 0;
st[p][++st[p][0]] = i;
id[0][i] = p, id[1][i] = st[p][0];
}
for (int i = 1; i <= n; ++i)
for (int j = 0; j < m; ++j)
a[i] = a[i] << 1 | Read();
for (int i = 1; i <= st[0][0]; i++)
if ((st[0][i] & a[1]) == st[0][i])
f[Now][i] = 1;
for (int i = 2; i <= n; i++)
{
for (int j = 0; j < m; j++)
{
Now ^= 1;
for (int k = 1; k <= st[0][0]; k++) // 枚举没有分割点的状态
{
if (!f[Now ^ 1][k]) continue;
int u = st[0][k] & (st[0][k] ^ (1 << j)), tmp = f[Now ^ 1][k];
f[Now ^ 1][k] = 0;
f[Now][id[1][u]] += tmp;
f[Now][id[1][u]] -= f[Now][id[1][u]] > mod? mod: 0;
if (!(a[i] & (1 << j)) || (st[0][k] & (1 << j)) || (j && (st[0][k] & (1 << j - 1)))) continue;
int x = id[1][st[0][k] | (1 << j)];
if (id[0][st[0][k] | (1 << j)]) x += st[0][0];
f[Now][x] += tmp;
f[Now][x] -= f[Now][x] > mod? mod: 0;
}
if (!j) continue;
for (int k = 1; k <= st[j][0]; k++) //有分割点
{
if (!f[Now ^ 1][k + st[0][0]]) continue;
int u = st[j][k] & (st[j][k] ^ (1 << j)), tmp = f[Now ^ 1][k + st[0][0]];
f[Now ^ 1][k + st[0][0]] = 0;
f[Now][id[1][u]] += tmp;
f[Now][id[1][u]] -= f[Now][id[1][u]] > mod? mod: 0;
if (!(a[i] & (1 << j)) || (st[j][k] & (1 << j)) || (j && (st[j][k] & (1 << j - 1)))) continue;
int x = id[1][st[j][k] | (1 << j)];
if (id[0][st[j][k] | (1 << j)]) x += st[0][0];
f[Now][x] += tmp;
f[Now][x] -= f[Now][x] > mod? mod: 0;
}
}
}
int ans = 0;
for (int k = 1; k <= st[0][0]; k++)
{
ans = ans + f[Now][k];
ans -= ans > mod ? mod: 0;
}
printf ("%d", ans);
return 0;
}