裏紙

ほぼ競プロ、たまに日記

CS Academy #57 - Binary Flips

問題

CS Academy

問題概要

N * Mのバイナリ行列Aがある。はじめ、行列の全要素は0である。この行列に対して、以下の操作をK回行う:

  • 行列のセル((i,j))を1つ指定し、i行目の全要素をバイナリ反転し、j列目の全要素を全要素をバイナリ反転する(よって、セル(i,j)は2回反転されるので変化しないことになる)。

操作をK回行った後に、値が1になっているセルの個数がS個になるような操作の方法は何通りあるか、10^9+7で割った余りを答えよ。

  • テストケース数 T \le 40
  •  1 \le N,M,K \le 3000
  •  0 \le S \le N * M

イデア

行に注目して、i行目に対して操作を行った回数の偶奇の値をR(i)、列に注目して、j列目に対して操作を行った回数の偶奇の値をC(j)と表すことにすると、A(i,j)=1となるには R(i) xor C(j) = 1 \Leftrightarrow R(i) \neq C(j)となることが条件になると言える。

まず、RCを独立にして考えてみる。

数列R (サイズ = N)に関して、dp[i][j] = i回操作を行った時に、1の個数がj個になるような操作の個数というものを考える。まず、初期値はdp[0][0] = 1で与えられる。

次に操作を起こる行に注目すると、0なら、1が1つ増えるので、0の個数を考慮するとdp[i+1][j+1] += dp[i][j]*(N-j)と配ることが出来る。逆に1の部分に対して操作をするならdp[i+1][j-1] += dp[i][j]*jと配ることが出来る。

RCに対して、このdp配列の前計算はそれぞれO(N^2)O(M^2)で行うことが出来る(それぞれの配列の名前をdr,dcとしておく)。

この前計算が終わってしまえば、あとは1になる行の個数と列の個数を全探索する(O(NM))。それぞれr_1行、c_1列あるとすると1になるセルの個数は、(どちらかが0でもう片方が1であれば良いので)r_1 * (M- c_1 ) + c_1 * (N - r_1 )個となる。これがSに一致する時のdr[k][r1] * dc[k][c1]を足し上げていけば良いことになる。

実装(C++)

#include <bits/stdc++.h>
using namespace std;
using ll = long long;
#define rep(i,n) for(int (i)=0;(i)<(int)(n);++(i))

const ll mod = 1e9+7;

vector<ll> make_table(int n, int k)
{
    vector<ll> dp(n+1);
    dp[0] = 1;
    rep(i,k)
    {
        vector<ll> nx(n+1);
        rep(j,n+1)
        {
            if(j+1<=n) (nx[j+1]+=dp[j]*(n-j))%=mod;
            if(j-1>=0) (nx[j-1]+=dp[j]*j)%=mod;
        }
        dp = nx;
    }
    return dp;
}

ll solve(int n, int m, int k, int s)
{
    vector<ll> dr = make_table(n,k), dc = make_table(m,k);
    ll ans = 0;
    rep(r1,n+1)rep(c1,m+1)
    {
        if(r1*(m-c1) + c1*(n-r1) == s) (ans += dr[r1]*dc[c1])%=mod;
    }
    return ans;
}

int main()
{
    int T;
    cin >>T;
    while(T--)
    {
        int n,m,k,s;
        cin >>n >>m >>k >>s;
        cout << solve(n,m,k,s) << endl;
    }
    return 0;
}