AtCoderのABC042Dを解いてみた

ABC問題のABC042D

競技プログラミングAtCoderの過去問にある、ABC042のDをやってみた。

問題文

縦 H マス、横 W マスのマス目があります。 いろはちゃんは、今一番左上のマス目にいます。 そして、右か下に1マス移動することを繰り返し、一番右下のマス目へと移動します。 ただし、下から A 個以内、かつ左から B 個以内のマス目へは移動することは出来ません。

移動する方法は何通りあるか求めてください。

なお、答えは非常に大きくなることがあるので、答えは \( 10^9+7 \) で割ったあまりを出力してください。

制約

  • \( 1 \leq H,W \leq 100000 \)
  • \( 1 \leq A < H \)
  • \( 1 \leq B < W \)

問題の本文はこちら

解答

最初は試しに全探索で試みる。各マス目において右か下にしか移動できないので、深さ優先探索でゴールを目指す。その際、移動が禁じられているエリアには入らないようにする。

void search_recursive(int x, int y, int H, int W, 
                      int A, int B, 
                      uint64_t &goal_counter) {
	
        if (x == W && y == H) { // stop condition, goal
		goal_counter++;
		return;
	}

	if (x == W+1 || y == H+1 
       || (x <= B && y == H-A+1)) {
		return;
	}

	search_recursive(x+1, y, H, W, A, B, goal_counter);
	search_recursive(x, y+1, H, W, A, B, goal_counter);
}


int main() {
	int H, W, A, B;
	std::cin >> H >> W >> A >> B;

	uint64_t goal_counter = 0LL;
	search_recursive(1, 1, H, W, A, B, goal_counter);
	std::cout << goal_counter % (1000000000+7) 
                  << std::endl;
}

結果、これだとWとHが大きい値になると制限時間までに間に合わない。次にやってみたのは、組合せを使った計算。スタート地点からゴールに辿り着くのに、どのルートを辿っても、トータルで右に W – 1 回と、下に H – 1 回移動することになるので、W x H のマス目のなかで左上から右下に移動するルートの全パターンは

\( Total = {(W-1)+(H-1)}\choose{(W-1)} \)

で求めることが出来る。この全ルートから、禁止されているエリアを通るルート

\( Sub = \sum_{k=0}^{B-1} { {(W-1-k)+(A-1)}\choose{(A-1)} } { {k+(H-A-1)}\choose{k} } \)

を引いた値が答えになる。

int64_t fact(int n) {
	int64_t factorial = 1LL;

	for (int i = 1; i <= n; i++) {
		factorial = (factorial*i);
	}

	return factorial;
}

int64_t comb(int n, int r) {
	return fact(n) / (fact(r) * fact(n-r));
}

int main() {
	int H, W, A, B;
	std::cin >> H >> W >> A >> B;

	int64_t total = comb((H-1)+(W-1), (W-1));
	int64_t sub = comb((A-1)+(W-1), (A-1));
	for (int i = 1; i < B; i++) {
	 	int64_t c0 = comb((A-1)+(W-1-i), (A-1));
	 	int64_t c1 = comb(i+(H-A-1), i);
	 	int64_t m0 = (c0 * c1);
	 	sub = (sub + m0);
	}
	int64_t res = (total-sub) % (1000000000+7);

	std::cout << res << std::endl;
}

結果、これでも、やはりHとWが大きいと間に合わない。そもそも、階乗の計算がオーバーフローしてしまって、正しい値を出力できていない。階乗の計算は毎回呼び出す度に、同じ計算を再度行っている状態なので、そこに時間を取られている。前もって階乗のテーブルを作っておく手法でやってみた。

階乗の結果が大きくなるとオーバーフローしてしまうので、\( 10^9+7 \) の素数で剰余演算しながらテーブルを作っていく。組合せの計算には、階乗の割り算も含まれるのだが、割り算に対しては剰余演算の分配法則は成り立たないので、逆数とその階乗のテーブルも作っておく。

逆数の計算

  • \( D = dq + r \)  ユークリッド除法, D=Dividend, d=divisor, q=quotient, r=remainder)
  • \( M = iq + r \)  (\( M = 10^9+7 \), i = 逆数を計算したい整数
  • \( M \bmod M = (iq + r) \bmod M \)  (両サイドに剰余演算)
  • \( 0 = iq \bmod M + r \bmod M \)
  • \( r \bmod M = -iq \bmod M \)
  • \( i^{-1} r \bmod M = i^{-1} (-iq) \bmod M \)  (両サイドに\( i^{-1} \) を掛ける)
  • \( r^{-1} i^{-1} r \bmod M = r^{-1} (-q) \bmod M \)  (同じく\( r^{-1} \) を掛ける)
  • \( i^{-1} \bmod M = -q r^{-1} \bmod M \)
  • \( i^{-1} \bmod M = -[M / i] (M \bmod i)^{-1} \bmod M \)  (\( q = [M/i], r = (M \bmod i) \))
  • \( i^{-1} \bmod M = -[M / i] (M \bmod i)^{-1} \bmod M + M \)  (負の剰余演算)
  • \( i^{-1} \bmod M = M – [M / i] (M \bmod i)^{-1} \bmod M \)  (整数 i の逆数)
const int64_t M = 1000000007;
const int MAX = 200000;

int64_t f[MAX], finv[MAX], inv[MAX];

void create_fact_table() {
	f[0] = 1;
	f[1] = 1;
	finv[0] = 1;
	finv[1] = 1;
	inv[1] = 1;

	for (int i = 2; i < MAX; i++) {
		f[i] = f[i-1] * i % M;
		inv[i] = M - inv[M % i] * (M / i) % M;
		finv[i] = finv[i-1] * inv[i] % M;
	}
}

int64_t comb_t(int n, int r) {
	return (f[n] * ((finv[r] * finv[n-r]) % M)) % M;
}

int main() {
	int H, W, A, B;
	std::cin >> H >> W >> A >> B;

	create_fact_table();

	int64_t total = comb_t((H-1)+(W-1), (W-1));
	int64_t sub = comb_t((A-1)+(W-1), (A-1));
	for (int i = 1; i < B; i++) {
		int64_t c0 = comb_t((A-1)+(W-1-i), (A-1));
		int64_t c1 = comb_t(i+(H-A-1), i);
		int64_t m0 = (c0 * c1) % M;
		sub = (sub + m0) % M;
	}
	int64_t res = ((total-sub) % M + M) % M;

// または、以下のコードも同じ
//
//	int64_t res = 0;
//	int i = 0;
//	int j = H-1;
//	while (i <= H-A-1 && j >= A) {
//		int64_t c0 = comb_t(B-1+i, i);
//		int64_t c1 = comb_t(W-B-1+j, j);
//		res = (res + ((c0 * c1) % M)) % M;
//		i++;
//		j--;
//	}

	std::cout << res << std::endl;
}

結果、これで解決できた。

タイトルとURLをコピーしました