AtCoder精選良問「D - Knight」Diff 1009
この記事はAtCoderの精選良問集マガジンの中の記事です。
私が良問だと思った問題を解説付きでまとめています。
問題リンク
問題概要
グリッド上の原点にナイトの駒がある
ナイトはマス(i, j)にあるとき、(i+1, j+2)または(i+2, j+1)のどちらかのマスに動かすことができる
ナイトの駒をマス(X, Y)まで移動させる方法は何通りあるか求める
答えは10^9 + 7で割った余りを出力する
制約:1≤X≤10^6、1≤Y≤10^6
考え方
初見ではDPっぽいと思う。
dp[i][j] マスi,jの行き方の通り数、遷移式はdp[i][j] = dp[i-1][j-2] + dp[i-2][j-1]とすれば正解は出そう。ただ、XとYが1,000,000迄と大きいので、二次元dpで行おうとすると、計算量が大きくなりすぎてしまう。
ではどうするかと言うと、組み合わせの概念を使って解く。
縦に2マス移動した回数をA回、縦に1マス移動した回数をB回とする。
Yが縦方向のインデックスだとすると、以下が成り立つ。
① Y = 2 x A + B
次に、Xは横方向のインデックス。
縦に1マス移動した時は横に2マス進んでいるので、以下が成り立つ。
② X = A + 2 x B
XとYは入力で与えられるので、Aを全探索して、①と②が成り立つBを求める。
合計の移動回数はA + B回。
そのうち2マス移動するようなAの選び方が何通りあるか、ということを求めると答えになる。これは二項係数、つまりnCrで計算すると解ける。
なお、今回は数が大きいので、余りを取りながら計算する必要がある。余りを使った割り算は普通にやるとうまくできない。
競プロ鉄則本などで紹介されている以下の性質を使う。
この性質を利用して、aが分子、bを分母だとすると、a * (b ^ M-2)をMで余りを取ると答えになる。b ^ M-2もこれまた計算量が大きいので、繰り返し二乗法のpower関数などを用意しておく。
実装
import { createInterface } from "readline";
import * as fs from "fs";
import { log } from "console";
import { exit } from "process";
// aのb乗を高速計算する関数
export const power = (a: bigint, b: bigint, mod: bigint): bigint => {
let ret = 1n;
if (b === 0n) return 1n % mod
let p = a;
const maxBit = b.toString(2).length
for (let i = 0n; i < maxBit; i++) {
const wari = 2n ** i;
if (wari > b) break
if ((b / wari) % 2n === 1n) {
ret = (ret * p) % mod;
}
p = (p * p) % mod;
}
return ret;
}
//a÷b を m で割った余りを返す関数
export const divisionMod = (a: bigint, b: bigint, mod: bigint): bigint => {
const powResult = power(b, mod - 2n, mod);
const result = (a * powResult) % mod;
return result < 0n ? result + mod : result;
}
function main() {
// 横、縦
const [x, y] = nextNums(2)
let tate2 = 0
let tate1 = 0
// 縦に2マス移動するのをa回行った時で全探索
for (let a = 0; a < Math.ceil(y / 2) + 1; a++) {
// 縦1マスで移動した回数
const b = y - 2 * a
// 成り立てばそれぞれの移動回数が決まる
if (b * 2 + a == x) {
tate2 = a
tate1 = b
}
}
const tot = BigInt(tate2 + tate1)
// どっちも0は移動方法がない
if (tot == 0n) {
log('0')
exit()
}
const mod = BigInt(10 ** 9 + 7)
const r = Math.max(tate2, tate1)
// 分子
let numerator = 1n
for (let i = 0n; i < r; i++) {
numerator *= (tot - i)
numerator %= mod
}
// 分母
let denominator = 1n
for (let i = 1n; i < r + 1; i++) {
denominator *= i
denominator %= mod
}
const ans = divisionMod(numerator, denominator, mod)
log(String(ans))
}
let inputs = "";
let inputArray: string[];
let currentIndex = 0;
function next() {
return inputArray[currentIndex++];
}
function nextNum() {
return +next();
}
function nextBigInt() {
return BigInt(next());
}
function nexts(length: number) {
const arr = [];
for (let i = 0; i < length; ++i) arr[i] = next();
return arr;
}
function nextNums(length: number) {
const arr = [];
for (let i = 0; i < length; ++i) arr[i] = nextNum();
return arr;
}
function nextBigInts(length: number) {
const arr = [];
for (let i = 0; i < length; ++i) arr[i] = nextBigInt();
return arr;
}
// デバッグ環境がWindowsであれば条件分岐する
if (process.env.OS == "Windows_NT") {
const stream = createInterface({
input: process.stdin,
output: process.stdout,
});
stream.on("line", (line) => {
inputs += line;
inputs += "\n";
});
stream.on("close", () => {
inputArray = inputs.split(/\s/);
main();
});
} else {
inputs = fs.readFileSync("/dev/stdin", "utf8");
inputArray = inputs.split(/\s/);
main();
}
この記事が気に入ったらサポートをしてみませんか?