题意

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

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

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

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

数据范围

对于所有数据,满足 \(1 \le N \le 2000\)。

题解

思维能力为 \(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\) 列之前。

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

然后直接转移是 \(\mathcal O(n^3)\) 的,由于只有相对的大小关系才有意义,可以前缀和优化到 \(\mathcal O(n^2)\) 。

总结

对于填的方案计数,如果相关元素不多,我们通常可以考虑 \(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; }

LOJ #2731. 「JOISC 2016 Day 1」棋盘游戏(dp)的更多相关文章

  1. Loj #2731 「JOISC 2016 Day 1」棋盘游戏

    Loj 2731 「JOISC 2016 Day 1」棋盘游戏 JOI 君有一个棋盘,棋盘上有 \(N\) 行 \(3\) 列 的格子.JOI 君有若干棋子,并想用它们来玩一个游戏.初始状态棋盘上至少 ...

  2. 「JOISC 2016 Day 1」棋盘游戏

    「JOISC 2016 Day 1」棋盘游戏 先判无解:第1,3行有连续的空格或四个角有空格. 然后可以发现有解的情况第1,3行可以在任意时间摆放. 对于某一列,若第2行放有棋子,那么显然可以把棋盘分 ...

  3. LOJ 2737 「JOISC 2016 Day 3」电报 ——思路+基环树DP

    题目:https://loj.ac/problem/2737 相连的关系形成若干环 / 内向基环树 .如果不是只有一个环的话,就得断开一些边使得图变成若干链.边的边权是以它为出边的点的点权. 基环树的 ...

  4. LOJ 2736 「JOISC 2016 Day 3」回转寿司 ——堆+分块思路

    题目:https://loj.ac/problem/2736 如果每个询问都是 l = 1 , r = n ,那么每次输出序列的 n 个数与本次操作的数的最大值即可.可以用堆维护. 不同区间的询问,可 ...

  5. [LOJ#2732] 「JOISC 2016 Day 2」雇佣计划

    参考博文 (不过个人感觉我讲的稍微更清楚一点) 题目就是让我们求图中满足数值大于等于B的连通块数量 然后我们可以尝试转换为求连通块两端所产生的“谷”的数量,显然一个连通块对谷可以贡献2的答案,最终答案 ...

  6. loj 2392「JOISC 2017 Day 1」烟花棒

    loj 答案显然满足二分性,先二分一个速度\(v\) 然后显然所有没有点火的都会往中间点火的人方向走,并且如果两个人相遇不会马上点火,要等到火快熄灭的时候才点火,所以这两个人之后应该在一起行动.另外有 ...

  7. 「JOISC 2016 Day 3」回转寿司

    https://loj.ac/problem/2736 题解 挺有意思的题. 考虑这种操作不好直接维护,还有时限比较长,所以考虑分块. 考虑一个操作对整个块的影响,无非就是可能把最大的拿走,再把新的元 ...

  8. loj2734「JOISC 2016 Day 2」女装大佬 || 洛谷P3615 如厕计划

    loj2734 洛谷P3615 http://218.5.5.242:9021/problem/185 不会做... 题解(来自ditoly): 这一步更详细的解释(来自kkksc03): 还是从后面 ...

  9. [LOJ #2833]「JOISC 2018 Day 1」帐篷

    题目大意:有一个$n\times m$的网格图,若一个人的同一行或同一列有人,他就必须面向那个人,若都无人,就可以任意一个方向.若一个人无法确定方向,则方案不合法,问不同的方案数.$n,m\leqsl ...

随机推荐

  1. 虚拟机下centos7.x简易命令大全与试玩体验

    OS: liunxversion: centos7.xdate: 2019-01-18 1. cd  /                               : 进入服务器根目录2. cd . ...

  2. linux $参数

    $# 是传给脚本的参数个数 $0 是脚本本身的名字 $1 是传递给该shell脚本的第一个参数 $2 是传递给该shell脚本的第二个参数 $@ 是传给脚本的所有参数的列表 $* 是以一个单字符串显示 ...

  3. AngularJS学习之旅—AngularJS Http(九)

    1.AngularJS XMLHttpRequest $http 是 AngularJS 中的一个核心服务,用于读取远程服务器的数据. eg: // 简单的 GET 请求,可以改为 POST $htt ...

  4. Docker 启动,进入容器,查看log命令

    1.启动一个容器 docker run -d -P training/webapp python app.py -d:让容器在后台运行. -P:将容器内部使用的网络端口映射到我们使用的主机上. 如果需 ...

  5. 二、Windows Server 2016 AD 组织单位、组、用户的创建

    简介: 组织单位简称OU,OU是(Organizational Unit)的缩写,组织单位是可以将用户.组.计算机和组织单位放入其中的容器.是可以指派组策略设置或委派管理权限的最小作用域或单元. 建立 ...

  6. HO6 Condo Insurance Policy

    The HO6 insurance Policy is the most common type of policy used to insure town homes and condos in t ...

  7. Linux文件目录

    简介: Linux 内核最初由芬兰的 Linus Torvalds 开发,后来他组建了团队,Linux 内核由这个团队维护. GNU 组织开发了很多核心软件和基础库,例如 GCC 编译器.C语言标准库 ...

  8. Kafka 0.11.0.0 实现 producer的Exactly-once 语义(英文)

    Exactly-once Semantics are Possible: Here’s How Kafka Does it I’m thrilled that we have hit an excit ...

  9. ERROR in static/js/0.5d7325513eec31f1e291.js from UglifyJs

    今天把vue项目打包是遇到这个问题.这是在服务器上打包时报的错误,本地打包不报错!很头疼!上网查了很多,发现有很多人和我遇到类似的问题,但是都没有解决我的问题!后来灵机一动,解决问题,这就跟大家说一下 ...

  10. 理解IO、NIO、 AIO

    转载:https://baijiahao.baidu.com/s?id=1586112410163034993&wfr=spider&for=pc nio 同步: 自己亲自出马持银行卡 ...