LOJ #2731. 「JOISC 2016 Day 1」棋盘游戏(dp)

题意

JOI 君有一个棋盘,棋盘上有 N3 列 的格子。JOI 君有若干棋子,并想用它们来玩一个游戏。初始状态棋盘上至少有一个棋子,也至少有一个空位。

游戏的目标是:在还没有放棋子的格子上依次放棋子,并填满整个棋盘。在某个格子上放置棋子必须满足以下条件之一:

  1. 这个格子的上下一格都放有棋子;
  2. 这个格子的左右一格都放有棋子。

JOI 君想知道有多少种从初始状态开始,并达到游戏目标的方案,这个答案可能会非常大。请你帮 JOI 君算出这个答案,并对 109+7 取模。

数据范围

对于所有数据,满足 1N2000

题解

思维能力为 0 的 sb 帮你把 doe 的讲解搬过来啦。

显然对于每个 x 的联通块是可以单独考虑的。

首先把无解的情况全部判掉,例如有两个 x 连在一起就不行,或者 x 在四个角落也是不行的。

那么我们的联通块的形状就形如:

x x x x xxxxxxxxx x x x

不难发现中间一行是一定连贯的,两边会不定时的出现 0/1/2 个空。

那么我们可以对于每个中间位进行考虑,因为上下位都是可以随时填进来的。

那么满足这个节点能填的方案只有,要么上下填了,要么左右填了,要么两个都填了。

这样的话我们可以设一个 f[i][j][0/1] 表示对于第 i 列中间空,它是在整个联通块第 j 个被填进去的,在它之前满足了 上下/左右 已经填好的方案数。

注意对于上下左右都填好的时候我们为了不算重,算到上下那里去。

然后对于转移,我们第 i 列如果选上下,那么 i+1 列上下填是不会限制的,但是对于左右填要强制 i 列中间出现在 i+1 列之前。

i 如果选左右,那么 i+1 列不能选左右,只能选上下,并且要强制 i+1 列出现在 i 列之前。

然后在联通块结束的时候用组合数算算方案就行了。

然后直接转移是 O(n3) 的,由于只有相对的大小关系才有意义,可以前缀和优化到 O(n2)

总结

对于填的方案计数,如果相关元素不多,我们通常可以考虑 i 填在全局第 j 个的方案数,然后转移的时候乘上一个排列/组合数就好了。

代码

具体转移的细节在代码中,可以参考。

#include <bits/stdc++.h> #define For(i, l, r) for (register int i = (l), i##end = (int)(r); i <= i##end; ++i) #define Fordown(i, r, l) for (register int i = (r), i##end = (int)(l); i >= i##end; --i) #define Rep(i, r) for (register int i = (0), i##end = (int)(r); i < i##end; ++i) #define Set(a, v) memset(a, v, sizeof(a)) #define Cpy(a, b) memcpy(a, b, sizeof(a)) #define debug(x) cout << #x << ": " << (x) << endl using namespace std; template<typename T> inline bool chkmin(T &a, T b) { return b < a ? a = b, 1 : 0; } template<typename T> inline bool chkmax(T &a, T b) { return b > a ? a = b, 1 : 0; } inline int read() { int x(0), sgn(1); char ch(getchar()); for (; !isdigit(ch); ch = getchar()) if (ch == '-') sgn = -1; for (; isdigit(ch); ch = getchar()) x = (x * 10) + (ch ^ 48); return x * sgn; } void File() { #ifdef zjp_shadow freopen ("2731.in", "r", stdin); freopen ("2731.out", "w", stdout); #endif } const int N = 4e3 + 1e2, Mod = 1e9 + 7; namespace Computation { inline int fpm(int x, int power) { int res = 1; for (; power; power >>= 1, x = 1ll * x * x % Mod) if (power & 1) res = 1ll * res * x % Mod; return res; } inline void add(int &x, int y) { if ((x += y) >= Mod) x -= Mod; } #define plus Plus inline int plus(int x, int y) { return (x += y) >= Mod ? x - Mod : x; } inline void sub(int &x, int y) { if ((x -= y) < 0) x += Mod; } inline int dec(int x, int y) { return (x -= y) < 0 ? x + Mod : x; } inline int mul(int x, int y) { return 1ll * x * y % Mod; } } using namespace Computation; int fac[N], ifac[N]; void Math_Init(int maxn) { fac[0] = ifac[0] = 1; For (i, 1, maxn) fac[i] = mul(fac[i - 1], i); ifac[maxn] = fpm(fac[maxn], Mod - 2); Fordown (i, maxn - 1, 1) ifac[i] = mul(ifac[i + 1], i + 1); } inline int comb(int n, int m) { if (n < 0 || m < 0 || n < m) return 0; return mul(mul(fac[n], ifac[n - m]), ifac[m]); } inline int perm(int n, int m) { if (n < 0 || m < 0 || n < m) return 0; return mul(fac[n], ifac[n - m]); } char S[3][N]; int n, f[N][N][2]; void get_pre(int x) { For (i, 1, n * 2) Rep (j, 2) add(f[x][i][j], f[x][i - 1][j]); //前缀和优化一维复杂度 } int main () { File(); n = read(); Rep (i, 3) scanf ("%s", S[i] + 1); Math_Init(n * 2 + 10); for (int i = 0; i <= 2; i += 2) For (j, 1, n) if (S[i][j] == 'x' && (S[i][j - 1] != 'o' || S[i][j + 1] != 'o')) return puts("0"), 0; int ans = 1, num = 0, tot = 0; if (S[1][1] == 'x') get_pre(f[1][1][0] = num = 1); For (i, 2, n) { int cur = (S[0][i] == 'x') + (S[2][i] == 'x'); if (S[1][i] == 'x') { if (S[1][i - 1] == 'o') { f[i][cur + 1][0] = fac[cur]; //横竖都可转移的时候,强制转移竖的 if (cur >= 1) f[i][1][1] = fac[cur]; if (cur >= 2) f[i][2][1] = 2; get_pre(i); num = cur + 1; continue; } num += cur + 1; For (j, 1, num) { add(f[i][j][0], mul(f[i - 1][num][0], perm(j - 1, cur))); //两个都是竖的,两个位置无先后要求,考虑当前上下插到中间前面的方案 add(f[i][j][0], mul(dec(f[i - 1][num][1], f[i - 1][max(0, j - (cur + 1))][1]), perm(j - 1, cur))); //这个竖,上个横,那么这个中间位要比上一列中间位先填 For (k, 1, cur) if (j + k <= num) add(f[i][j + k][1], mul(mul(fac[cur], comb(j + k - 1, k - 1)), //上下在中间位之前出现的方案数 与 上下互换的方案 mul(comb(max(0, num - j - k), cur + 1 - k), f[i - 1][j][0]))); //上下在中间位之后出现的方案数 以及 //强制横的先出现,竖的出现在 0 那里统计 } get_pre(i); } else { if (S[1][i - 1] == 'x') ans = mul(mul(ans, plus(f[i - 1][num][0], f[i - 1][num][1])), comb(tot += num, num)); //把当前联通块塞到前面的方案数 ans = mul(ans, perm(tot += cur, cur)); //注意对于联通块首的上下位的方案数要单独计算 } } if (S[1][n] == 'x') ans = mul(ans, mul(f[n][num][0], comb(tot += num, num))); //最后一个联通块可能没有统计,要统计上 printf ("%d\n", ans); return 0; }

__EOF__

本文作者zjp_shadow
本文链接https://www.cnblogs.com/zjp-shadow/p/10438622.html
关于博主:评论和私信会在第一时间回复。或者直接私信我。
版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!
声援博主:如果您觉得文章对您有帮助,可以点击文章右下角推荐一下。您的鼓励是博主的最大动力!
posted @   zjp_shadow  阅读(774)  评论(0编辑  收藏  举报
编辑推荐:
· 如何编写易于单元测试的代码
· 10年+ .NET Coder 心语,封装的思维:从隐藏、稳定开始理解其本质意义
· .NET Core 中如何实现缓存的预热?
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
阅读排行:
· 地球OL攻略 —— 某应届生求职总结
· 周边上新:园子的第一款马克杯温暖上架
· Open-Sora 2.0 重磅开源!
· 提示词工程——AI应用必不可少的技术
· .NET周刊【3月第1期 2025-03-02】
历史上的今天:
2018-02-26 BZOJ 1926: [Sdoi2010]粟粟的书架(主席树,二分答案)
点击右上角即可分享
微信分享提示