{ }codelper
動的計画法2次元リストP0339

まねできないビー玉取り

ビー玉が n 個、一つの山に積まれていて、二人が交互にビー玉を取る。
一回の番に 1 個以上 k 個以下を取らなければならず、残っているビー玉より多くは取れない。
相手が直前の番に取った個数と同じ個数は取れない。
いちばん最初の番にはこの制限がない。
自分の番に取れる個数が一つもない人が負ける。
ビー玉が一つも残っていないときはもちろん、ビー玉が残っていても取れる個数がすべてふさがれていれば負ける。
たとえば相手が今 1 個取って 1 個残ったなら、取れないので負ける。
二人とも、自分が勝てる方法があれば必ずそのように取る。
k は一つに決まっていて、n が q 個与えられる。
n ごとに、先手が最初の番に取ったとき、そのあと相手がどう取っても勝てる個数をすべて求めよ。
そのような個数を小さいものから空白一つで区切って一行に出力し、一つもなければ 0 を出力する。

入力
一行目に k と q が空白一つで区切られて与えられ、続く q 行に各行 n が一つずつ与えられる。
1 ≤ k ≤ 10、1 ≤ q ≤ 9,000、1 ≤ n ≤ 9,000。
すべて整数である
出力
n ごとに一行ずつ、先手が勝てる最初の番の個数(小さいものから、空白一つで区切る)、または 0
例 1 · stdin
4 5
2
3
5
6
9
stdout
1 2
3
0
1 3
2 4
例 2 · stdin
1 3
1
2
7
stdout
1
1
1
main.py
入力(stdin)

実行すると、これらの行が順に input()・標準入力に渡されます(実行中に入力を求めません)。

$ 準備完了

コードを入力して解くにはパソコンで開いてください。モバイルでは問題と解答を見られます。

ほかの言語では?言語別の解答を見る · 8 言語の解答

残りのビー玉の数だけでは勝てるかどうか分かりません。
「何個がふさがれているか」も一緒に書いておく必要があるので、表は二次元です。
win[r][f] を「ビー玉が r 個残り、f 個がふさがれているとき、今取る人が勝つか」とし、r が小さいものから埋めます(動的計画法)。
t 個取ると相手は win[r - t][t] に立つので、ふさがれていない t の中にその値が偽のものが一つでもあれば勝ちです。
一つ目の例の n = 2 を見ると、2 個全部取っても勝ち、1 個取っても相手は 1 個をまねできないので勝ちです。
だから 1 2 です。
落とし穴その一:まねできないという規則を抜くと、よくある「(k + 1) の倍数を残す」遊びになり、n = 2 で 2、n = 6 で 1 しか出ません。
落とし穴その二:ふさがれた人が負けるかわりに、ふさがれた個数をそのまま取らせると、n = 2 で 2 しか出ず、二つ目の例(k = 1)の n = 2 は 0 になります。
落とし穴その三:残りより多く取ることを止めないと番号が負になります。
Python は win[-1] がエラーなしに最後の行を読むので、n = 3 に 3 4 のような間違った答えを出します。
JS・TS は undefined になり、続く [t] で TypeError で止まり、Craykon もその番号はないと言って止まります。
Java・C# は例外を投げ、C・C++ では未定義動作です。
落とし穴その四:表の作り方です。
Python の [[False] * (k + 1)] * (top + 1) と JS・TS の new Array(top + 1).fill(new Array(k + 1).fill(false)) は同じ一つの行を何度も指すので、一マス変えるとすべての行が変わります(一つ目の例が全部 1 になります)。
Python は [[False] * (k + 1) for _ in range(top + 1)]、JS・TS は Array.from({ length: top + 1 }, () => …) で行ごとに新しく作ります。
Java new boolean[top + 1][k + 1]、C# new bool[top + 1, k + 1]、C++ vector<vector<char>>(top + 1, vector<char>(k + 1))、C のグローバルな二次元配列は最初から行が別々です。
Craykon の 新リスト(終わり + 1, 新リスト(k + 1, 偽)) も行ごとに別々に作ってくれます。
落とし穴その五:上から再帰で解いて結果を覚えておく方法は、n が数千だと再帰も数千段になります。
Python は既定の上限 1,000 を超えて RecursionError で止まるので、下から表を埋めます。
落とし穴その六:勝てる最初の手がなければ、空行ではなく 0 を書きます。

Pythonmain.py · 26 行
import sys  def main():    data = sys.stdin.read().split()    k, q = int(data[0]), int(data[1])    asks = [int(x) for x in data[2:2 + q]]    top = max(asks)     # win[r][f]: 구슬이 r개 남았고 f개는 가져갈 수 없을 때(f = 0이면 제한 없음), 지금 둘 사람이 이기는가    win = [[False] * (k + 1) for _ in range(top + 1)]    for r in range(1, top + 1):        # good: 가져가면 상대가 지는 자리로 가는 개수들(제한은 아직 따지지 않음)        good = [t for t in range(1, min(k, r) + 1) if not win[r - t][t]]        row = win[r]        for f in range(k + 1):            row[f] = len(good) >= 2 or (len(good) == 1 and good[0] != f)     out = []    for n in asks:        first = [t for t in range(1, min(k, n) + 1) if not win[n - t][t]]        out.append(" ".join(map(str, first)) if first else "0")    print("\n".join(out))  main()
JavaScriptmain.js · 31 行
const data = require("fs").readFileSync(0, "utf8").split(/\s+/).filter(Boolean).map(Number);const k = data[0], q = data[1];const asks = data.slice(2, 2 + q);const top = Math.max(...asks); // win[r][f]: 구슬이 r개 남았고 f개는 가져갈 수 없을 때(f = 0이면 제한 없음), 지금 둘 사람이 이기는가// fill(배열)은 같은 배열 하나를 모든 줄에 넣으므로, 줄마다 새로 만든다const win = Array.from({ length: top + 1 }, () => new Array(k + 1).fill(false));for (let r = 1; r <= top; r++) {  // 가져가면 상대가 지는 자리로 가는 개수가 몇 가지인지, 한 가지뿐이면 그것이 무엇인지  let count = 0, only = 0;  for (let t = 1; t <= k && t <= r; t++) {    if (!win[r - t][t]) {      count++;      only = t;    }  }  for (let f = 0; f <= k; f++) {    win[r][f] = count >= 2 || (count === 1 && only !== f);  }} const out = [];for (const n of asks) {  const first = [];  for (let t = 1; t <= k && t <= n; t++) {    if (!win[n - t][t]) first.push(t);  }  out.push(first.length > 0 ? first.join(" ") : "0");}console.log(out.join("\n"));
TypeScriptmain.ts · 31 行
const data = require("fs").readFileSync(0, "utf8").split(/\s+/).filter(Boolean).map(Number);const k = data[0], q = data[1];const asks = data.slice(2, 2 + q);const top = Math.max(...asks); // win[r][f]: 구슬이 r개 남았고 f개는 가져갈 수 없을 때(f = 0이면 제한 없음), 지금 둘 사람이 이기는가// fill(배열)은 같은 배열 하나를 모든 줄에 넣으므로, 줄마다 새로 만든다const win: boolean[][] = Array.from({ length: top + 1 }, () => new Array(k + 1).fill(false));for (let r = 1; r <= top; r++) {  // 가져가면 상대가 지는 자리로 가는 개수가 몇 가지인지, 한 가지뿐이면 그것이 무엇인지  let count = 0, only = 0;  for (let t = 1; t <= k && t <= r; t++) {    if (!win[r - t][t]) {      count++;      only = t;    }  }  for (let f = 0; f <= k; f++) {    win[r][f] = count >= 2 || (count === 1 && only !== f);  }} const out: string[] = [];for (const n of asks) {  const first: number[] = [];  for (let t = 1; t <= k && t <= n; t++) {    if (!win[n - t][t]) first.push(t);  }  out.push(first.length > 0 ? first.join(" ") : "0");}console.log(out.join("\n"));
JavaMain.java · 40 行
import java.util.*; public class Main {    public static void main(String[] args) {        Scanner in = new Scanner(System.in);        int k = in.nextInt(), q = in.nextInt();        int[] asks = new int[q];        int top = 0;        for (int i = 0; i < q; i++) {            asks[i] = in.nextInt();            top = Math.max(top, asks[i]);        }         // win[r][f]: 구슬이 r개 남았고 f개는 가져갈 수 없을 때(f = 0이면 제한 없음), 지금 둘 사람이 이기는가        boolean[][] win = new boolean[top + 1][k + 1];   // 모두 false로 시작한다        for (int r = 1; r <= top; r++) {            // 가져가면 상대가 지는 자리로 가는 개수가 몇 가지인지, 한 가지뿐이면 그것이 무엇인지            int count = 0, only = 0;            for (int t = 1; t <= k && t <= r; t++) {                if (!win[r - t][t]) {                    count++;                    only = t;                }            }            for (int f = 0; f <= k; f++) {                win[r][f] = count >= 2 || (count == 1 && only != f);            }        }         StringBuilder out = new StringBuilder();        for (int n : asks) {            StringJoiner first = new StringJoiner(" ");            for (int t = 1; t <= k && t <= n; t++) {                if (!win[n - t][t]) first.add(String.valueOf(t));            }            out.append(first.length() > 0 ? first.toString() : "0").append('\n');        }        System.out.print(out);    }}
C#Main.cs · 51 行
using System;using System.Collections.Generic;using System.Text; class Program{    static void Main()    {        string[] data = Console.In.ReadToEnd().Split((char[])null, StringSplitOptions.RemoveEmptyEntries);        int k = int.Parse(data[0]), q = int.Parse(data[1]);        var asks = new int[q];        int top = 0;        for (int i = 0; i < q; i++)        {            asks[i] = int.Parse(data[2 + i]);            top = Math.Max(top, asks[i]);        }         // win[r, f]: 구슬이 r개 남았고 f개는 가져갈 수 없을 때(f = 0이면 제한 없음), 지금 둘 사람이 이기는가        var win = new bool[top + 1, k + 1];   // 네모난 2차원 배열, 모두 false로 시작한다        for (int r = 1; r <= top; r++)        {            // 가져가면 상대가 지는 자리로 가는 개수가 몇 가지인지, 한 가지뿐이면 그것이 무엇인지            int count = 0, only = 0;            for (int t = 1; t <= k && t <= r; t++)            {                if (!win[r - t, t])                {                    count++;                    only = t;                }            }            for (int f = 0; f <= k; f++)            {                win[r, f] = count >= 2 || (count == 1 && only != f);            }        }         var output = new StringBuilder();        foreach (int n in asks)        {            var first = new List<int>();            for (int t = 1; t <= k && t <= n; t++)            {                if (!win[n - t, t]) first.Add(t);            }            output.Append(first.Count > 0 ? string.Join(" ", first) : "0").Append('\n');        }        Console.Write(output);    }}
C++main.cpp · 44 行
#include <algorithm>#include <iostream>#include <vector>using namespace std; int main() {    ios::sync_with_stdio(false);    cin.tie(nullptr);    int k, q;    cin >> k >> q;    vector<int> asks(q);    for (int &n : asks) cin >> n;    int top = *max_element(asks.begin(), asks.end());     // win[r][f]: 구슬이 r개 남았고 f개는 가져갈 수 없을 때(f = 0이면 제한 없음), 지금 둘 사람이 이기는가    vector<vector<char>> win(top + 1, vector<char>(k + 1, 0));   // 줄마다 따로 복사된다    for (int r = 1; r <= top; r++) {        // 가져가면 상대가 지는 자리로 가는 개수가 몇 가지인지, 한 가지뿐이면 그것이 무엇인지        int count = 0, only = 0;        for (int t = 1; t <= k && t <= r; t++) {            if (!win[r - t][t]) {                count++;                only = t;            }        }        for (int f = 0; f <= k; f++) {            win[r][f] = count >= 2 || (count == 1 && only != f);        }    }     for (int n : asks) {        bool printed = false;        for (int t = 1; t <= k && t <= n; t++) {            if (!win[n - t][t]) {                if (printed) cout << ' ';                cout << t;                printed = true;            }        }        if (!printed) cout << 0;        cout << '\n';    }    return 0;}
Cmain.c · 47 行
#include <stdio.h>#include <stdlib.h> #define MAX_N 9000#define MAX_K 10 /* win[r][f]: 구슬이 r개 남았고 f개는 가져갈 수 없을 때(f = 0이면 제한 없음), 지금 둘 사람이 이기는가 */static char win[MAX_N + 1][MAX_K + 1];   /* 전역 배열은 모두 0으로 시작한다 */ int main(void) {    int k, q;    if (scanf("%d %d", &k, &q) != 2) return 1;    int *asks = malloc(sizeof(int) * q);    int top = 0;    for (int i = 0; i < q; i++) {        scanf("%d", &asks[i]);        if (asks[i] > top) top = asks[i];    }     for (int r = 1; r <= top; r++) {        /* 가져가면 상대가 지는 자리로 가는 개수가 몇 가지인지, 한 가지뿐이면 그것이 무엇인지 */        int count = 0, only = 0;        for (int t = 1; t <= k && t <= r; t++) {            if (!win[r - t][t]) {                count++;                only = t;            }        }        for (int f = 0; f <= k; f++) {            win[r][f] = count >= 2 || (count == 1 && only != f);        }    }     for (int i = 0; i < q; i++) {        int n = asks[i], printed = 0;        for (int t = 1; t <= k && t <= n; t++) {            if (!win[n - t][t]) {                printf(printed ? " %d" : "%d", t);                printed = 1;            }        }        if (!printed) printf("0");        printf("\n");    }    free(asks);    return 0;}
Craykonmain.cray · 52 行
첫줄 = コンソール.数読み();k = 첫줄[0];q = 첫줄[1];묻는수 = [];끝 = 0;繰り返す (i = 0; i < q; i++){    n = コンソール.行読み().数に();    묻는수.追加(n);    끝 = 数学.大きい方(끝, n);} // 이김[r][f]: 구슬이 r개 남았고 f개는 가져갈 수 없을 때(f = 0이면 제한 없음), 지금 둘 사람이 이기는가이김 = 新リスト(끝 + 1, 新リスト(k + 1, 偽));繰り返す (r = 1; r <= 끝; r++){    // 가져가면 상대가 지는 자리로 가는 개수가 몇 가지인지, 한 가지뿐이면 그것이 무엇인지    가짓수 = 0;    하나뿐 = 0;    繰り返す (t = 1; t <= k && t <= r; t++)    {        もし (!이김[r - t][t])        {            가짓수 += 1;            하나뿐 = t;        }    }    繰り返す (f = 0; f <= k; f++)    {        이김[r][f] = 가짓수 >= 2 || 가짓수 == 1 && 하나뿐 != f;    }} それぞれ (n の中 묻는수){    첫수 = [];    繰り返す (t = 1; t <= k && t <= n; t++)    {        もし (!이김[n - t][t])        {            첫수.追加(t);        }    }    もし (첫수.個数 > 0)    {        コンソール.書く(첫수);    }    でなければ    {        コンソール.書く(0);    }}