非常牛的一道计数题啊。
下面是部分分:
- 10 pts:爆搜枚举,但是我看错题了,没写出来。
- [30,50] pts:dp,不会,没写,我是 dp 低手。
接下来是正解部分。
一定不要像我一样没看到相邻两个格子颜色不能相同,因为这至关重要。
我们发现两行非常不好做,想办法转化成 1 行,注意到同一列只有 RG
GR
BR
RB
GB
BG
六种情况,且每两种正好没有包括 B
G
和 R
,于是我们想到,用在这一列没有出现过的那种颜色去代表这一列的
两行。这样,我们就有 m-r 个红色, m-b 个蓝色以及 m-g 个绿色。
为了方便,接下来仍然认为有 R 个 R
, G 个 G
以及 B 个 B
。
假设我们最终搜出来有 x 种答案,那么我们可以把原棋盘每列两行颠倒得到新的 x 种合法方案,所以最后要乘 2 。
考虑如何在新的一行上统计答案,我们注意到,这些 R
把原序列划分成了若干个待填充的空段,且两两不相邻,具体的,如果有 R 个 R
:
- 若这个序列的首尾都是
R
,那么一共有 R-1 段。 - 如果首、尾有一个是
R
,那么有恰好 R 段(注意首、尾要算两种情况)。 - 如果首尾都不是
R
,那么有 R+1 段。
所以我们外层套一个循环暴力枚举有多少段,设当前段数为 n 。
注意到待填的都是 G
和 B
,同时他们两个必须间隔出现,所以段长的奇偶性会导致一些差别:
- 如果段长是偶数,这一段
G
和B
的数量相同。 - 如果是奇数,这一段
G
和B
的数量差 1 ,具体地,如果是G
开头,那么G
多 1 ,否则反之。
我们继续设长为偶数的段有 x_0 个,长为奇数且以 G
开头的有 x_1 个,以 B
开头的是 x_2 个。
三元一次方程不好解,所以我们再用一层循环枚举 x_0\in[0,n] ,则 x_1 、 x_2 显然满足如下关系:
由于 n 、 x_0 、 G 、 B 都是常量,那么 x_1 和 x_2 也可以求出。
求出之后干点什么呢?
假设我们已经确定好这些空段的顺序,那么一共有多少种方案啊?
注意需要保证这些段填完之后都是非空的,所以我们给一个偶数段强行填入一个 GB
(或者 BG
),一个 x_1 段强行填入一个 G
,一个 x_2 段强行填入一个 B
,我们发现接下来我们只需要往所有段中捆绑填 BG
或者 GB
即可。
此时我们剩下 G-x_0-x_1 个 G
和 B-x_0-x_2 个 B
,当然这两个值应该是相等的。
接下来采取插板法,一共有 G-x_0-x_1 个绑包,分成 n 部分,可以有空(因为我们已经保证段非空了),则有 G-x_0-x_1+n-1\choose n-1 种方法。
所以单个排列的方案数是 G-x_0-x_1+n-1\choose n-1 的,那么如果在已知 x_0 、 x_1 、 x_2 的情况下,又有多少种排列呢?显然是 {n\choose x_0}\times{n-x_0\choose x_1}=\frac{n!}{x_0!x_1!x_2!} 的,因为注意到 x_0+x_1+x_2=n 。
最后还有一个容易被忽略的事情是,长为偶数的段可以任意选择 G
还是 B
开头,所以如果有 x_0 个偶数段的话,还要乘一个 2^{x_0} 。
场切的巨佬真的好牛好强。
实现需要预处理阶乘数组及其逆元,注意要更新下标为 0 时的数值。
#include <bits/stdc++.h>
#define int long long
#define f(i ,m ,n ,x) for (int i = (m) ;i <= (n) ;i += (x))
using namespace std ;
template < typename T > inline void read ( T &x ) {
x = 0 ;
bool flag (0) ;
register char ch = getchar () ;
while (! isdigit (ch)) {
flag = ch == '-' ;
ch = getchar () ;
}
while (isdigit (ch)) {
x = (x << 1) + (x << 3) + (ch ^ 48) ;
ch = getchar () ;
}
flag ? x = - x : 0 ;
}
template < typename T ,typename ...Args >
inline void read ( T &x ,Args &...tmp ) {
read (x) ,read (tmp...) ;
}
const int N = 1e7 + 7 ,mod = 1e9 + 7 ;
int m ,r ,g ,b ,jc[N + 1] ,inv[N + 1] ,ans ;
namespace meloddd {
inline int qpow (int base ,int p = mod - 2) {
int ans = 1ll ;
while (p) {
if (p & 1ll)
(ans *= base) %= mod ;
(base *= base) %= mod ;
p >>= 1ll ;
}
return ans ;
}
inline void pre () {
jc[0] = 1ll ; // attention
f (i ,1 ,N ,1)
jc[i] = jc[i - 1] * i % mod ;
}
inline void rep () {
inv[N] = qpow (jc[N]) ;
for (int i = N - 1 ;~ i ;i --) // ~ i but not i
inv[i] = inv[i + 1] * (i + 1) % mod ;
}
inline int C (int n ,int m) {
return jc[n] * inv[n - m] % mod * inv[m] % mod ;
} // 组合数公式展开一下
}
using meloddd :: qpow ;
using meloddd :: C ;
using meloddd :: pre ;
using meloddd :: rep ;
signed main () {
freopen ("board.in" ,"r" ,stdin) ;
freopen ("board.out" ,"w" ,stdout) ;
read (m ,r ,g ,b) ;
pre () ,rep () ;
if (max ({r ,g ,b}) > m)
return puts ("0") ,0 ;
r = m - r ,g = m - g ,b = m - b ;
if (r < g) swap (r ,g) ;
if (g < b) swap (g ,b) ;
if (r < b) swap (r ,b) ;
for (int n : {r - 1 ,r ,r ,r + 1}) {
if (n < 1) continue ;
f (x0 ,0 ,n ,1) {
static int x1 ,x2 ;
x1 = (n - x0 + g - b) >> 1ll ;
x2 = (n - x0 + b - g) >> 1ll ;
if (x1 < 0 || x2 < 0 || x1 + x2 + x0 != n || x1 - x2 != g - b) continue ;
int num = g - x0 - x1 ;
if (num < 0) continue ;
(ans += C (num + n - 1 ,n - 1) * jc[n] % mod * inv[x0] % mod
* inv[x1] % mod * inv[x2] % mod * qpow (2ll ,x0) % mod) %= mod ;
}
}
cout << (ans << 1ll) % mod << '\n' ;
return 0 ;
}