結果

問題 No.3418 【絶望】30個並列ごちゃ混ぜHit&Blowで遊ぼう!
コンテスト
ユーザー butsurizuki
提出日時 2025-12-14 20:04:48
言語 C++23
(gcc 13.3.0 + boost 1.89.0)
結果
AC  
実行時間 101 ms / 5,000 ms
コード長 18,327 bytes
記録
記録タグの例:
初AC ショートコード 純ショートコード 純主流ショートコード 最速実行時間
コンパイル時間 4,440 ms
コンパイル使用メモリ 328,176 KB
実行使用メモリ 25,972 KB
スコア 9,964,197
平均クエリ数 358.03
最終ジャッジ日時 2025-12-25 01:33:54
合計ジャッジ時間 14,304 ms
ジャッジサーバーID
(参考情報)
judge5 / judge4
純コード判定しない問題か言語
このコードへのチャレンジ
(要ログイン)
ファイルパターン 結果
other AC * 100
権限があれば一括ダウンロードができます

ソースコード

diff #
raw source code

// By GPT 5.2 Pro
#include <bits/stdc++.h>
using namespace std;

/*
  方針(重要アイデア):
  - クエリ q の数字集合を Q (|Q|=5) とする。
  - ある未解決の秘密文字列 S の数字集合を D (|D|=5) とする。
  - D==Q のときに限り、S と q の共通数字数は 5 なので必ず h+b=5 になる。
    (数字は相異・長さ 5 なので)
  - すでに当てた S は以後どんなクエリでも (5,0) が返る。
  - したがって「h+b==5 の個数」から、クエリ集合 Q と同じ数字集合を持つ秘密文字列が何本あるか数えられる。
    ただし、過去に当てた本数(= (5,0) の本数)ぶんは常に h+b==5 に混入するので差し引く。
*/

struct Reply {
    array<pair<int,int>,30> hb{};
    int solvedCount = 0;     // (5,0) の数
    bool allSolved = false;  // hb[0]==(5,0) なら全て (5,0)
    bool invalid = false;    // (-1,-1) が来たら不正
};

static int g_solvedCount = 0;                       // これまでに当てた本数
static unordered_set<string> g_solvedStrings;       // 当てた文字列集合
static unordered_set<string> g_asked;               // 既に投げたクエリ(無駄撃ち回避)
static array<int, 1<<10> g_solvedByMask{};          // 数字集合(mask)ごとの当てた本数
static mt19937 rng(1234567);

static inline int maskFromString(const string& s){
    int m=0;
    for(char c: s) m |= 1<<(c-'0');
    return m;
}

static inline int hitCount(const string& a, const string& b){
    int h=0;
    for(int i=0;i<5;i++) if(a[i]==b[i]) h++;
    return h;
}

// クエリを出して 30 行読む(インタラクティブ)
static Reply ask(const string& q){
    cout << q << "\n" << flush;

    Reply rep;
    for(int i=0;i<30;i++){
        int h,b;
        if(!(cin >> h >> b)){
            // EOF: ここまでで終了
            exit(0);
        }
        rep.hb[i] = {h,b};
    }
    if(rep.hb[0].first == -1 && rep.hb[0].second == -1){
        rep.invalid = true;
        return rep;
    }
    int sc=0;
    for(auto &p: rep.hb) if(p.first==5 && p.second==0) sc++;
    rep.solvedCount = sc;
    rep.allSolved = (rep.hb[0].first==5 && rep.hb[0].second==0);
    return rep;
}

static void recordSolvedString(const string& s){
    if(g_solvedStrings.insert(s).second){
        int m = maskFromString(s);
        g_solvedByMask[m]++;
    }
}

// 「今回のクエリの数字集合」と一致する(未解決の)秘密文字列たちの hit 値 multiset を取り出す。
// 具体的には、h+b==5 のペア列から「以前に当てた本数(prevSolved)」だけ (5,0) を除去し、残りの h を返す。
// ※クエリと同じ数字集合のものだけが未解決で h+b==5 になる。既解決は常に (5,0) で混ざるので引く。
static vector<int> extractHitsSameDigitSet(const Reply& rep, int prevSolved){
    vector<pair<int,int>> sum5;
    sum5.reserve(30);
    for(auto &p: rep.hb){
        if(p.first >= 0 && p.first + p.second == 5) sum5.push_back(p);
    }
    int remove = prevSolved;
    vector<int> hits;
    hits.reserve(sum5.size());
    for(auto &p: sum5){
        if(p.first==5 && p.second==0 && remove>0){
            remove--;
        }else{
            hits.push_back(p.first);
        }
    }
    sort(hits.begin(), hits.end());
    return hits;
}

static vector<int> digitsFromMask(int mask){
    vector<int> d;
    for(int x=0;x<10;x++) if(mask&(1<<x)) d.push_back(x);
    return d;
}

static string digitsToAscendingQuery(const vector<int>& d){
    string s;
    for(int x: d) s.push_back(char('0'+x));
    return s;
}

static vector<string> allPermutationsOfDigits(vector<int> d){
    sort(d.begin(), d.end());
    vector<string> perms;
    do{
        string s;
        for(int x: d) s.push_back(char('0'+x));
        perms.push_back(s);
    }while(next_permutation(d.begin(), d.end()));
    // 5! = 120
    return perms;
}

struct MaskInfo {
    int cnt = 0;                 // この数字集合を持つ秘密文字列の本数
    string scanQuery;            // スキャンで投げたクエリ(昇順)
    vector<int> scanHitsAll;     // scanQuery に対する「この集合の文字列」全本数ぶんの hit multiset(サイズ cnt)
};

// フォールバック: この数字集合の 120 順列を順に投げて remaining 本当てる
static void bruteForceSolveMask(const MaskInfo& info, int mask, int remaining){
    if(remaining<=0) return;
    auto digits = digitsFromMask(mask);
    auto perms = allPermutationsOfDigits(digits);

    for(const string& q: perms){
        if(remaining<=0) return;
        if(g_solvedStrings.count(q)) continue;
        if(g_asked.count(q)) continue; // なるべく無駄を避ける

        int prevSolved = g_solvedCount;
        Reply rep = ask(q);
        g_asked.insert(q);

        if(rep.invalid) exit(0);
        if(rep.solvedCount > prevSolved){
            recordSolvedString(q);
            remaining--;
        }
        g_solvedCount = rep.solvedCount;
        if(rep.allSolved) exit(0);
    }
}

// remaining==1 を「候補絞り込み(minimax)」で解く
static void solveSingleInMask(const MaskInfo& info, int mask){
    int remaining = info.cnt - g_solvedByMask[mask];
    if(remaining!=1) return;

    auto digits = digitsFromMask(mask);
    auto perms = allPermutationsOfDigits(digits);

    // scanHitsAll から、すでにこの mask で解いた分を引いて「残り1本の scan hit」を得る
    vector<int> scanHits = info.scanHitsAll;
    for(const string& s: g_solvedStrings){
        if(maskFromString(s)==mask){
            int h = hitCount(s, info.scanQuery);
            auto it = find(scanHits.begin(), scanHits.end(), h);
            if(it!=scanHits.end()) scanHits.erase(it);
        }
    }

    vector<int> candidates;
    candidates.reserve(120);

    if(scanHits.size()==1){
        int h0 = scanHits[0];
        for(int i=0;i<(int)perms.size();i++){
            if(g_solvedStrings.count(perms[i])) continue;
            if(hitCount(perms[i], info.scanQuery)==h0) candidates.push_back(i);
        }
    }else{
        // 万一崩れたら全候補から
        for(int i=0;i<(int)perms.size();i++){
            if(g_solvedStrings.count(perms[i])) continue;
            candidates.push_back(i);
        }
    }

    // 安全策
    if(candidates.empty()){
        bruteForceSolveMask(info, mask, 1);
        return;
    }

    // 追加クエリで絞る(最大 12 回程度で打ち切ってフォールバック)
    for(int step=0; step<12; step++){
        if(g_solvedCount>=30) exit(0);
        if(candidates.size()==1){
            string guess = perms[candidates[0]];
            if(!g_asked.count(guess) && !g_solvedStrings.count(guess)){
                int prevSolved = g_solvedCount;
                Reply rep = ask(guess);
                g_asked.insert(guess);
                if(rep.invalid) exit(0);
                if(rep.solvedCount > prevSolved){
                    recordSolvedString(guess);
                }
                g_solvedCount = rep.solvedCount;
                if(rep.allSolved) exit(0);
            }
            return;
        }

        // minimax(候補を最も割る質問を探す)
        int bestQ = -1;
        int bestMax = INT_MAX;

        for(int qi=0; qi<(int)perms.size(); qi++){
            const string& q = perms[qi];
            if(g_asked.count(q)) continue;
            int bucket[6]={0,0,0,0,0,0};
            for(int ci: candidates){
                int h = hitCount(perms[ci], q);
                bucket[h]++;
            }
            int mx=0;
            for(int k=0;k<6;k++) mx=max(mx, bucket[k]);
            if(mx < bestMax){
                bestMax = mx;
                bestQ = qi;
            }
        }
        if(bestQ==-1){
            // すべて聞いてしまった等: 先頭候補を投げる
            bestQ = candidates[0];
        }

        string q = perms[bestQ];
        int prevSolved = g_solvedCount;
        Reply rep = ask(q);
        g_asked.insert(q);

        if(rep.invalid) exit(0);

        // この mask のクエリなので、増えていればこの mask の解
        if(rep.solvedCount > prevSolved){
            recordSolvedString(q);
        }

        // hit の観測(この mask に残っている 1 本ぶん)
        vector<int> hits = extractHitsSameDigitSet(rep, prevSolved);
        g_solvedCount = rep.solvedCount;
        if(rep.allSolved) exit(0);

        if(hits.size()!=1){
            bruteForceSolveMask(info, mask, 1);
            return;
        }

        int obs = hits[0];
        // obs==5 なら q が正解で解けた
        if(obs==5) return;

        vector<int> nextCand;
        nextCand.reserve(candidates.size());
        for(int ci: candidates){
            if(hitCount(perms[ci], q)==obs) nextCand.push_back(ci);
        }
        candidates.swap(nextCand);

        if(candidates.empty()){
            bruteForceSolveMask(info, mask, 1);
            return;
        }
    }

    // 打ち切りフォールバック
    bruteForceSolveMask(info, mask, 1);
}

// remaining==2 or 3 を「部分集合列挙→マルチセット一致」で解く
template<int R>
static void solveMultiInMask(const MaskInfo& info, int mask){
    int remaining = info.cnt - g_solvedByMask[mask];
    if(remaining!=R) return;

    auto digits = digitsFromMask(mask);
    auto perms = allPermutationsOfDigits(digits);

    // scanHitsAll から、すでにこの mask で解いた分を引いて「残りR本の scan hit」を得る
    vector<int> scanHits = info.scanHitsAll;
    for(const string& s: g_solvedStrings){
        if(maskFromString(s)==mask){
            int h = hitCount(s, info.scanQuery);
            auto it = find(scanHits.begin(), scanHits.end(), h);
            if(it!=scanHits.end()) scanHits.erase(it);
        }
    }
    if((int)scanHits.size()!=R){
        // 崩れていたら brute force
        bruteForceSolveMask(info, mask, remaining);
        return;
    }
    sort(scanHits.begin(), scanHits.end());

    // 候補部分集合を作る(scanQuery に対する hit multiset が一致するものだけ)
    vector<int> avail;
    avail.reserve(120);
    for(int i=0;i<(int)perms.size();i++){
        if(g_solvedStrings.count(perms[i])) continue;
        avail.push_back(i);
    }

    struct Sub { array<uint8_t,R> a; };
    vector<Sub> subs;

    auto sc = info.scanQuery;

    if constexpr (R==2){
        for(int xi=0; xi<(int)avail.size(); xi++){
            for(int yi=xi+1; yi<(int)avail.size(); yi++){
                int i=avail[xi], j=avail[yi];
                int h1=hitCount(perms[i], sc);
                int h2=hitCount(perms[j], sc);
                array<int,2> hs{h1,h2};
                sort(hs.begin(), hs.end());
                if(hs[0]==scanHits[0] && hs[1]==scanHits[1]){
                    Sub s; s.a = {(uint8_t)i,(uint8_t)j};
                    subs.push_back(s);
                }
            }
        }
    } else { // R==3
        for(int xi=0; xi<(int)avail.size(); xi++){
            for(int yi=xi+1; yi<(int)avail.size(); yi++){
                for(int zi=yi+1; zi<(int)avail.size(); zi++){
                    int i=avail[xi], j=avail[yi], k=avail[zi];
                    int h1=hitCount(perms[i], sc);
                    int h2=hitCount(perms[j], sc);
                    int h3=hitCount(perms[k], sc);
                    array<int,3> hs{h1,h2,h3};
                    sort(hs.begin(), hs.end());
                    if(hs[0]==scanHits[0] && hs[1]==scanHits[1] && hs[2]==scanHits[2]){
                        Sub s; s.a = {(uint8_t)i,(uint8_t)j,(uint8_t)k};
                        subs.push_back(s);
                    }
                }
            }
        }
    }

    if(subs.empty()){
        bruteForceSolveMask(info, mask, remaining);
        return;
    }

    // 追加クエリで絞る(最大 8 回)
    for(int step=0; step<8; step++){
        if(g_solvedCount>=30) exit(0);
        if(subs.size()==1) break;

        // クエリ候補を少数サンプルして minimax
        vector<int> qCand;
        qCand.reserve(120);
        for(int qi=0; qi<(int)perms.size(); qi++){
            if(g_asked.count(perms[qi])) continue;
            qCand.push_back(qi);
        }
        if(qCand.empty()) break;

        shuffle(qCand.begin(), qCand.end(), rng);
        int K = min<int>((int)qCand.size(), 25);

        int bestQ = qCand[0];
        int bestMax = INT_MAX;

        for(int t=0;t<K;t++){
            int qi = qCand[t];
            // outcome code を数える
            // R==2: code=a*6+b (a<=b)
            // R==3: code=a*36+b*6+c (a<=b<=c)
            static vector<int> cnt;
            int maxCode = (R==2 ? 36 : 216);
            cnt.assign(maxCode, 0);

            for(const auto& s: subs){
                array<int,R> hs;
                for(int r=0;r<R;r++){
                    hs[r] = hitCount(perms[(int)s.a[r]], perms[qi]);
                }
                sort(hs.begin(), hs.end());
                int code = 0;
                if constexpr (R==2){
                    code = hs[0]*6 + hs[1];
                }else{
                    code = hs[0]*36 + hs[1]*6 + hs[2];
                }
                cnt[code]++;
            }
            int mx=0;
            for(int v: cnt) mx=max(mx,v);
            if(mx < bestMax){
                bestMax = mx;
                bestQ = qi;
            }
        }

        string q = perms[bestQ];
        int prevSolved = g_solvedCount;
        Reply rep = ask(q);
        g_asked.insert(q);

        if(rep.invalid) exit(0);

        // もし解けたなら(この mask のどれかに一致)、一旦戻って remaining を減らして解き直す
        if(rep.solvedCount > prevSolved){
            recordSolvedString(q);
            g_solvedCount = rep.solvedCount;
            if(rep.allSolved) exit(0);
            return;
        }

        vector<int> obs = extractHitsSameDigitSet(rep, prevSolved);
        g_solvedCount = rep.solvedCount;
        if(rep.allSolved) exit(0);

        if((int)obs.size()!=R){
            bruteForceSolveMask(info, mask, remaining);
            return;
        }
        sort(obs.begin(), obs.end());

        vector<Sub> next;
        next.reserve(subs.size());
        for(const auto& s: subs){
            array<int,R> hs;
            for(int r=0;r<R;r++){
                hs[r] = hitCount(perms[(int)s.a[r]], q);
            }
            sort(hs.begin(), hs.end());
            bool ok=true;
            for(int r=0;r<R;r++) if(hs[r]!=obs[r]) ok=false;
            if(ok) next.push_back(s);
        }
        subs.swap(next);

        if(subs.empty()){
            bruteForceSolveMask(info, mask, remaining);
            return;
        }
    }

    if(subs.size()==1){
        // 特定できたので順に当てる
        auto s = subs[0];
        int need = info.cnt - g_solvedByMask[mask];
        for(int r=0;r<R;r++){
            if(need<=0) break;
            string guess = perms[(int)s.a[r]];
            if(g_solvedStrings.count(guess)) continue;
            if(g_asked.count(guess)) {
                // 既に聞いたのに解けてないなら矛盾 → brute
                bruteForceSolveMask(info, mask, need);
                return;
            }
            int prevSolved = g_solvedCount;
            Reply rep = ask(guess);
            g_asked.insert(guess);

            if(rep.invalid) exit(0);

            if(rep.solvedCount > prevSolved){
                recordSolvedString(guess);
                need--;
            }else{
                // 外した → brute
                g_solvedCount = rep.solvedCount;
                if(rep.allSolved) exit(0);
                bruteForceSolveMask(info, mask, need);
                return;
            }
            g_solvedCount = rep.solvedCount;
            if(rep.allSolved) exit(0);
        }
        return;
    }

    // まだ複数候補なら brute
    bruteForceSolveMask(info, mask, remaining);
}

int main(){
    ios::sync_with_stdio(false);
    cin.tie(nullptr);

    // 5 数字集合(10C5=252)を列挙
    vector<int> masks;
    for(int m=0; m<(1<<10); m++){
        if(__builtin_popcount((unsigned)m)==5) masks.push_back(m);
    }

    array<MaskInfo, 1<<10> info;

    // ---- Phase 1: 252(ただし途中で 30 本分揃ったら打ち切り)スキャン ----
    int cumulative = 0;
    for(int mask: masks){
        if(cumulative>=30) break;

        auto digits = digitsFromMask(mask);
        string q = digitsToAscendingQuery(digits); // この mask の代表(昇順)
        if(g_asked.count(q)) continue; // 念のため

        int prevSolved = g_solvedCount;
        Reply rep = ask(q);
        g_asked.insert(q);

        if(rep.invalid) return 0;

        // この mask の本数 = (h+b==5 の個数) - prevSolved
        int sum5 = 0;
        for(auto &p: rep.hb){
            if(p.first + p.second == 5) sum5++;
        }
        int cnt = sum5 - prevSolved;
        if(cnt < 0) cnt = 0; // 念のため

        info[mask].cnt = cnt;
        info[mask].scanQuery = q;
        info[mask].scanHitsAll = extractHitsSameDigitSet(rep, prevSolved);

        if(rep.solvedCount > prevSolved){
            // q がこの mask のどれかに一致して新規に解けた
            recordSolvedString(q);
        }

        g_solvedCount = rep.solvedCount;
        if(rep.allSolved) return 0;

        cumulative += cnt;
    }

    // ---- Phase 2: mask ごとに順列を当てに行く ----
    for(int mask: masks){
        int need = info[mask].cnt - g_solvedByMask[mask];
        if(need<=0) continue;

        // 小さいところは賢く、でかいところは brute
        while(true){
            if(g_solvedCount>=30) return 0;
            int rem = info[mask].cnt - g_solvedByMask[mask];
            if(rem<=0) break;

            if(rem==1) solveSingleInMask(info[mask], mask);
            else if(rem==2) solveMultiInMask<2>(info[mask], mask);
            else if(rem==3) solveMultiInMask<3>(info[mask], mask);
            else {
                bruteForceSolveMask(info[mask], mask, rem);
                break;
            }
        }
    }

    // 念のため(普通ここまで来ない)
    return 0;
}
0