結果
| 問題 | No.3418 【絶望】30個並列ごちゃ混ぜHit&Blowで遊ぼう! |
| コンテスト | |
| ユーザー |
|
| 提出日時 | 2025-12-16 12:20:50 |
| 言語 | C++23 (gcc 13.3.0 + boost 1.89.0) |
| 結果 |
AC
|
| 実行時間 | 1,115 ms / 5,000 ms |
| コード長 | 24,165 bytes |
| 記録 | |
| コンパイル時間 | 4,649 ms |
| コンパイル使用メモリ | 319,984 KB |
| 実行使用メモリ | 26,228 KB |
| スコア | 9,995,841 |
| 平均クエリ数 | 41.59 |
| 最終ジャッジ日時 | 2025-12-25 01:58:44 |
| 合計ジャッジ時間 | 85,514 ms |
|
ジャッジサーバーID (参考情報) |
judge2 / judge5 |
| 純コード判定しない問題か言語 |
(要ログイン)
| ファイルパターン | 結果 |
|---|---|
| other | AC * 100 |
ソースコード
#include <bits/stdc++.h>
using namespace std;
// ------------------------------------------------------------
// 30-parallel mixed Hit&Blow solver (interactive / local sim)
// ------------------------------------------------------------
static constexpr int LEN = 5;
static constexpr int DIG = 10;
static constexpr int N = 30240; // 10P5
static constexpr int K = 21; // number of (hit,blow) categories
struct Code {
array<uint8_t, LEN> d;
uint16_t mask; // 10-bit
string s;
};
static int HB_ID[6][6];
static array<pair<int,int>, K> ID_HB;
static inline int popcount16(uint16_t x){
return __builtin_popcount((unsigned)x);
}
static vector<Code> CODES;
static void build_codes(){
CODES.reserve(N);
for(int a=0;a<10;a++) for(int b=0;b<10;b++) if(b!=a)
for(int c=0;c<10;c++) if(c!=a && c!=b)
for(int d=0;d<10;d++) if(d!=a && d!=b && d!=c)
for(int e=0;e<10;e++) if(e!=a && e!=b && e!=c && e!=d){
Code x;
x.d = {(uint8_t)a,(uint8_t)b,(uint8_t)c,(uint8_t)d,(uint8_t)e};
x.mask = (1u<<a)|(1u<<b)|(1u<<c)|(1u<<d)|(1u<<e);
x.s.resize(LEN);
x.s[0] = char('0'+a);
x.s[1] = char('0'+b);
x.s[2] = char('0'+c);
x.s[3] = char('0'+d);
x.s[4] = char('0'+e);
CODES.push_back(std::move(x));
}
// sanity
if((int)CODES.size()!=N){
cerr << "code size mismatch: " << CODES.size() << endl;
exit(0);
}
}
static inline uint8_t hb_cat(int i, int q){
const auto &A = CODES[i].d;
const auto &B = CODES[q].d;
int hit = (A[0]==B[0]) + (A[1]==B[1]) + (A[2]==B[2]) + (A[3]==B[3]) + (A[4]==B[4]);
int common = popcount16(CODES[i].mask & CODES[q].mask);
int blow = common - hit;
return (uint8_t)HB_ID[hit][blow];
}
// Timer
struct Timer {
chrono::steady_clock::time_point st;
Timer(){ reset(); }
void reset(){ st = chrono::steady_clock::now(); }
double ms() const {
return chrono::duration<double, std::milli>(chrono::steady_clock::now()-st).count();
}
};
// Simple Dinic for small graphs
struct Dinic {
struct Edge{ int to; int rev; int cap; };
int n;
vector<vector<Edge>> g;
vector<int> level, it;
Dinic(int n=0){ init(n);}
void init(int n_){ n=n_; g.assign(n,{}); level.assign(n,0); it.assign(n,0);}
void add_edge(int fr,int to,int cap){
Edge a{to,(int)g[to].size(),cap};
Edge b{fr,(int)g[fr].size(),0};
g[fr].push_back(a);
g[to].push_back(b);
}
bool bfs(int s,int t){
fill(level.begin(), level.end(), -1);
queue<int> q; level[s]=0; q.push(s);
while(!q.empty()){
int v=q.front(); q.pop();
for(const auto &e:g[v]) if(e.cap>0 && level[e.to]<0){
level[e.to]=level[v]+1;
q.push(e.to);
}
}
return level[t]>=0;
}
int dfs(int v,int t,int f){
if(v==t) return f;
for(int &i=it[v]; i<(int)g[v].size(); i++){
Edge &e=g[v][i];
if(e.cap>0 && level[e.to]==level[v]+1){
int ret=dfs(e.to,t, min(f,e.cap));
if(ret>0){
e.cap-=ret;
g[e.to][e.rev].cap+=ret;
return ret;
}
}
}
return 0;
}
int max_flow(int s,int t){
int flow=0;
while(bfs(s,t)){
fill(it.begin(), it.end(), 0);
while(true){
int f=dfs(s,t,1e9);
if(!f) break;
flow += f;
}
}
return flow;
}
};
// Local judge (for simulation)
#ifdef LOCAL
struct LocalJudge{
vector<int> hidden; // size 30
vector<char> already; // N
int found=0;
LocalJudge(uint64_t seed){
already.assign(N,0);
// sample 30 distinct
std::mt19937_64 rng(seed);
vector<int> all(N);
iota(all.begin(), all.end(), 0);
for(int i=0;i<30;i++){
int j = std::uniform_int_distribution<int>(i, N-1)(rng);
swap(all[i], all[j]);
}
hidden.assign(all.begin(), all.begin()+30);
}
array<int,K> ask(int qidx){
array<int,K> hist{};
for(int s: hidden){
if(already[s]){
hist[HB_ID[5][0]]++;
}else{
hist[hb_cat(s,qidx)]++;
}
}
// update already
bool hit = false;
for(int s: hidden) if(s==qidx){ hit=true; break; }
if(hit && !already[qidx]){
already[qidx]=1;
found++;
}
return hist;
}
};
#endif
// ------------------------------------------------------------
// Solver
// ------------------------------------------------------------
struct Solver {
// state
vector<char> alive, asked, forced;
vector<int> alive_list;
vector<int> qidxs; // asked indices
vector<array<int,K>> cnt; // residual histogram for remaining unknown set
vector<vector<uint8_t>> cats; // cats[qid][i] category
vector<uint32_t> zeroMask; // per query
deque<int> forcedQ;
vector<double> w;
int found=0;
Timer *timer=nullptr;
double time_limit_ms = 4950.0;
Solver(){
alive.assign(N,1);
asked.assign(N,0);
forced.assign(N,0);
alive_list.resize(N);
iota(alive_list.begin(), alive_list.end(), 0);
w.assign(N, 0.0);
}
inline bool time_ok() const {
return timer ? (timer->ms() < time_limit_ms) : true;
}
void rebuild_alive(){
vector<int> nxt;
nxt.reserve(alive_list.size());
for(int x: alive_list) if(alive[x]) nxt.push_back(x);
alive_list.swap(nxt);
}
void add_forced(int idx){
if(!alive[idx] || asked[idx] || forced[idx]) return;
forced[idx]=1;
forcedQ.push_back(idx);
}
// Heuristic exact enumeration for the final phase.
// Tries to build (some) full completions of the remaining hidden set
// consistent with *all* hist constraints, and uses vote frequency
// to pick a high-confidence next query.
//
// This is only used when the remaining count is very small, so the depth is small.
int refine_by_enumeration(int n_rem, int max_solutions, double budget_ms){
if(n_rem <= 0) return -1;
int m = (int)qidxs.size();
if(m == 0) return -1;
if((int)alive_list.size() > 3500) return -1;
if(!timer) return -1;
// Keep a small safety margin.
double end_ms = min(time_limit_ms - 40.0, timer->ms() + budget_ms);
if(end_ms <= timer->ms()) return -1;
// Copy needs (residual histograms)
vector<array<int,K>> need = cnt;
// Choose a pivot query: the one that has the tightest needed bucket.
int pivot = m-1;
int best_bucket = INT_MAX;
for(int qid=0; qid<m; qid++){
int ac[K] = {0};
const auto &cat = cats[qid];
for(int x: alive_list){
if(alive[x]) ac[cat[x]]++;
}
int local_best = INT_MAX;
for(int r=0;r<K;r++) if(need[qid][r] > 0) local_best = min(local_best, ac[r]);
if(local_best < best_bucket){
best_bucket = local_best;
pivot = qid;
}
}
vector<uint8_t> used(N, 0);
vector<int> cur;
cur.reserve(n_rem);
vector<uint16_t> freq(N, 0);
int sol_cnt = 0;
// A small helper: forward-check on pivot only (cheap)
auto pivot_feasible = [&]()->bool{
int avail[K] = {0};
const auto &catp = cats[pivot];
for(int x: alive_list){
if(!alive[x] || used[x]) continue;
avail[catp[x]]++;
}
for(int r=0;r<K;r++) if(need[pivot][r] > avail[r]) return false;
return true;
};
function<void(int)> dfs = [&](int depth){
if(sol_cnt >= max_solutions) return;
if(timer->ms() > end_ms) return;
if(depth == n_rem){
sol_cnt++;
for(int x: cur) freq[x]++;
return;
}
if(!pivot_feasible()) return;
// Choose next category in pivot with smallest available pool
int best_r = -1;
int best_sz = INT_MAX;
const auto &catp = cats[pivot];
for(int r=0;r<K;r++){
if(need[pivot][r] <= 0) continue;
int sz = 0;
for(int x: alive_list){
if(!alive[x] || used[x]) continue;
if(catp[x] == r) sz++;
}
if(sz < need[pivot][r]) return; // impossible
if(sz < best_sz){
best_sz = sz;
best_r = r;
if(best_sz == need[pivot][r]) break;
}
}
if(best_r < 0) return;
// Collect candidates in that bucket
vector<int> cand;
cand.reserve(best_sz);
for(int x: alive_list){
if(!alive[x] || used[x]) continue;
if(catp[x] == best_r) cand.push_back(x);
}
// Prefer high-probability candidates first.
sort(cand.begin(), cand.end(), [&](int a, int b){ return w[a] > w[b]; });
for(int x: cand){
if(sol_cnt >= max_solutions) break;
if(timer->ms() > end_ms) break;
used[x] = 1;
cur.push_back(x);
bool ok = true;
// decrement needs for all queries
for(int qid=0; qid<m; qid++){
uint8_t c = cats[qid][x];
int &v = need[qid][c];
v--;
if(v < 0) ok = false;
}
if(ok) dfs(depth+1);
// undo
for(int qid=0; qid<m; qid++){
uint8_t c = cats[qid][x];
need[qid][c]++;
}
cur.pop_back();
used[x] = 0;
}
};
dfs(0);
if(sol_cnt == 0) return -1;
// Choose the candidate that appears most frequently in enumerated completions.
int best_idx = -1;
int best_f = -1;
for(int x: alive_list){
if(!alive[x] || asked[x]) continue;
int f = freq[x];
if(f > best_f){
best_f = f;
best_idx = x;
}
}
// Use only when we have a strong signal and some sample size.
if(sol_cnt >= 4 && best_idx >= 0 && best_f * 5 >= sol_cnt * 4) return best_idx; // >=80%
return -1;
}
void compute_cats_for_query(int q){
vector<uint8_t> v(N);
for(int i=0;i<N;i++) v[i]=hb_cat(i,q);
cats.push_back(std::move(v));
}
void update_zeroMask(int qid){
uint32_t m=0;
for(int r=0;r<K;r++) if(cnt[qid][r]==0) m |= (1u<<r);
zeroMask[qid]=m;
}
bool prune_by_zeroMask(int qid){
uint32_t zm = zeroMask[qid];
if(!zm) return false;
bool changed=false;
vector<int> nxt;
nxt.reserve(alive_list.size());
const auto &cat = cats[qid];
for(int x: alive_list){
if(!alive[x]) continue;
if(zm & (1u<<cat[x])){
alive[x]=0;
changed=true;
}else{
nxt.push_back(x);
}
}
alive_list.swap(nxt);
return changed;
}
// peel discovered code idx from all previous histograms (excluding latest query)
void peel_found(int idx){
int m = (int)qidxs.size();
for(int qid=0; qid<m-1; qid++){
uint8_t c = cats[qid][idx];
if(cnt[qid][c]>0){
cnt[qid][c]--;
if(cnt[qid][c]==0){
zeroMask[qid] |= (1u<<c);
prune_by_zeroMask(qid);
}
}
}
}
// single-query forced elements: if all remaining in a category must be chosen
void single_forced(){
int m = (int)qidxs.size();
static int ac[K];
for(int qid=0; qid<m; qid++){
memset(ac,0,sizeof(ac));
const auto &cat = cats[qid];
for(int x: alive_list) ac[cat[x]]++;
uint32_t eqMask=0;
for(int r=0;r<K;r++) if(cnt[qid][r]>0 && ac[r]==cnt[qid][r]) eqMask |= (1u<<r);
if(!eqMask) continue;
for(int x: alive_list) if(eqMask & (1u<<cat[x])) add_forced(x);
}
}
// Pair pruning using one feasible flow + residual reachability.
// Returns true if any candidate removed.
bool pair_prune(int qa, int qb){
// supplies / demands
const auto &A = cnt[qa];
const auto &B = cnt[qb];
int total=0;
for(int r=0;r<K;r++) total += A[r];
if(total==0) return false; // nothing left
// cap matrix
static int cap[K][K];
static int flow[K][K];
for(int i=0;i<K;i++) for(int j=0;j<K;j++) cap[i][j]=0;
const auto &catA = cats[qa];
const auto &catB = cats[qb];
for(int x: alive_list){
cap[catA[x]][catB[x]]++;
}
// Build max flow network
// nodes: S=0, rows 1..K, cols 1+K .. 1+2K-1, T=1+2K
int S=0, A0=1, B0=1+K, T=1+2*K;
Dinic din(T+1);
for(int a=0;a<K;a++) if(A[a]) din.add_edge(S, A0+a, A[a]);
for(int b=0;b<K;b++) if(B[b]) din.add_edge(B0+b, T, B[b]);
// store edge indices to extract flow
static int eid[K][K];
for(int a=0;a<K;a++) for(int b=0;b<K;b++) eid[a][b]=-1;
for(int a=0;a<K;a++){
int u=A0+a;
for(int b=0;b<K;b++) if(cap[a][b]){
int v=B0+b;
eid[a][b] = (int)din.g[u].size();
din.add_edge(u,v,cap[a][b]);
}
}
int fval = din.max_flow(S,T);
if(fval!=total){
// should not happen; be conservative
return false;
}
// Extract flow on each A->B edge: reverse edge cap is the used flow
for(int a=0;a<K;a++) for(int b=0;b<K;b++) flow[a][b]=0;
for(int a=0;a<K;a++){
int u=A0+a;
for(int b=0;b<K;b++) if(eid[a][b]!=-1){
auto &e = din.g[u][eid[a][b]];
// reverse edge
int v = e.to;
auto &rev = din.g[v][e.rev];
int used = rev.cap;
flow[a][b]=used;
}
}
// Residual reachability on bipartite nodes (rows + cols)
// node: row a -> id a (0..K-1), col b -> id K+b (K..2K-1)
constexpr int M = 2*K;
uint64_t reach[M];
for(int u=0;u<M;u++) reach[u] = (1ULL<<u);
for(int a=0;a<K;a++){
for(int b=0;b<K;b++) if(cap[a][b]){
int fa = flow[a][b];
if(fa < cap[a][b]){
// forward residual row->col
reach[a] |= (1ULL << (K+b));
}
if(fa > 0){
// backward residual col->row
reach[K+b] |= (1ULL << a);
}
}
}
// transitive closure (Floyd with bitsets)
for(int k=0;k<M;k++){
uint64_t mk = (1ULL<<k);
for(int u=0;u<M;u++){
if(reach[u] & mk) reach[u] |= reach[k];
}
}
// Build impossible / forced_full masks per row a.
static uint32_t impMask[K];
static uint32_t fullMask[K];
for(int a=0;a<K;a++){ impMask[a]=0; fullMask[a]=0; }
for(int a=0;a<K;a++){
for(int b=0;b<K;b++) if(cap[a][b]){
int fa = flow[a][b];
if(fa==0){
bool canInc = (fa < cap[a][b]) && (reach[K+b] & (1ULL<<a));
if(!canInc) impMask[a] |= (1u<<b);
}
if(fa==cap[a][b]){
bool canDec = (fa > 0) && (reach[a] & (1ULL<<(K+b)));
if(!canDec) fullMask[a] |= (1u<<b);
}
}
}
bool changed=false;
vector<int> nxt;
nxt.reserve(alive_list.size());
for(int x: alive_list){
uint8_t a = catA[x];
uint8_t b = catB[x];
if(impMask[a] & (1u<<b)){
alive[x]=0;
changed=true;
}else{
nxt.push_back(x);
if(fullMask[a] & (1u<<b)) add_forced(x);
}
}
alive_list.swap(nxt);
return changed;
}
// Run pair pruning against the newest query with many previous queries.
void pair_closure_latest(){
int m = (int)qidxs.size();
if(m<=1) return;
int last = m-1;
// When the number of queries is still small, doing a full pair-closure
// is affordable and often removes a lot of candidates.
if(m <= 12){
for(int a=0; a<m; a++){
for(int b=a+1; b<m; b++){
if(!time_ok()) return;
pair_prune(a,b);
}
}
return;
}
// Process all previous queries vs last (biggest impact)
for(int i=0;i<last;i++){
if(!time_ok()) break;
pair_prune(i,last);
}
// Extra closure among a recent window.
if(!time_ok()) return;
int W = 6; // recent window
int st = max(0, last-W);
for(int a=st; a<last; a++){
for(int b=a+1; b<last; b++){
if(!time_ok()) return;
pair_prune(a,b);
}
}
}
// IPF weight update (more sweeps when time allows)
void ipf_update(int sweeps){
int m = (int)qidxs.size();
int n_rem = 30 - found;
if(n_rem<=0) return;
if(alive_list.empty()) return;
double base = (double)n_rem / (double)alive_list.size();
for(int x: alive_list) w[x]=base;
static double sum[K];
static double factor[K];
for(int it=0; it<sweeps && time_ok(); it++){
for(int qid=0; qid<m; qid++){
memset(sum, 0, sizeof(sum));
const auto &cat = cats[qid];
for(int x: alive_list) sum[cat[x]] += w[x];
for(int r=0;r<K;r++){
if(sum[r]>0) factor[r] = (double)cnt[qid][r] / sum[r];
else factor[r] = 1.0;
}
for(int x: alive_list) w[x] *= factor[cat[x]];
}
}
}
int choose_next(){
// forced first
while(!forcedQ.empty()){
int x=forcedQ.front();
forcedQ.pop_front();
if(alive[x] && !asked[x]) return x;
}
// otherwise IPF
int sweeps = 110;
if(timer){
double rem = time_limit_ms - timer->ms();
if(rem < 1000) sweeps = 60;
if(rem < 350) sweeps = 25;
}
ipf_update(sweeps);
#ifdef USE_FINISH_ENUM
// Late-game refinement (experimental): enumerate a few feasible completions and vote.
{
int n_rem = 30 - found;
if(n_rem <= 6 && (int)alive_list.size() <= 4000){
double rem_ms = timer ? (time_limit_ms - timer->ms()) : 0.0;
if(rem_ms > 250.0){
int cand = refine_by_enumeration(n_rem, /*max_solutions*/28, /*budget_ms*/28.0);
if(cand != -1) return cand;
}
}
}
#endif
int best=-1;
double bw=-1.0;
for(int x: alive_list){
if(!alive[x] || asked[x]) continue;
double wx = w[x];
if(wx > bw){ bw=wx; best=x; }
}
if(best==-1){
// fallback
for(int x=0;x<N;x++) if(alive[x] && !asked[x]) return x;
}
return best;
}
// Process judge response for query q and histogram hist
void apply_response(int q, const array<int,K>& hist){
// invalid query check is handled by judge (all -1,-1) in interactive; in local never.
int new_found = hist[HB_ID[5][0]];
bool hit_new = (new_found > found);
found = new_found;
// Residual histogram for remaining unknown (excluding found ones)
array<int,K> resid = hist;
resid[HB_ID[5][0]] -= found;
// Add query
qidxs.push_back(q);
compute_cats_for_query(q);
cnt.push_back(resid);
zeroMask.push_back(0);
update_zeroMask((int)qidxs.size()-1);
// mark asked and remove from alive
asked[q]=1;
if(alive[q]) alive[q]=0;
rebuild_alive();
int qid_latest = (int)qidxs.size()-1;
prune_by_zeroMask(qid_latest);
if(hit_new){
// peel from previous constraints
peel_found(q);
}
// pair pruning & forced detection
pair_closure_latest();
single_forced();
}
};
// ------------------------------------------------------------
// Interactive I/O helpers
// ------------------------------------------------------------
#ifndef LOCAL
static inline bool valid_query(const string& s){
if((int)s.size()!=LEN) return false;
bool used[10]={0};
for(char c: s){
if(c<'0' || c>'9') return false;
if(used[c-'0']) return false;
used[c-'0']=true;
}
return true;
}
#endif
int main(){
// init HB mapping
memset(HB_ID, -1, sizeof(HB_ID));
int id=0;
for(int h=0;h<=5;h++){
for(int b=0;b<=5-h;b++){
HB_ID[h][b]=id;
ID_HB[id]={h,b};
id++;
}
}
build_codes();
#ifdef LOCAL
// Local simulation benchmark
int T = 30; // number of random cases (keep it moderate for local benchmarking)
uint64_t seed0 = 1;
long long sumQ=0;
long long score=0;
for(int tc=0; tc<T; tc++){
LocalJudge judge(seed0 + tc*10007ULL);
Solver solver;
Timer t;
solver.timer = &t;
while(solver.found < 30){
int q = solver.choose_next();
auto hist = judge.ask(q);
solver.apply_response(q,hist);
if(!solver.time_ok()){
// stop heavy computations, but keep querying naively
// (should not happen in local)
solver.time_limit_ms = 1e9;
}
if((int)solver.qidxs.size() > 200){
cerr << "too many queries" << endl;
break;
}
}
int Q = (int)solver.qidxs.size();
sumQ += Q;
score += 100000 - Q;
// (no per-case prints)
}
cerr << "T="<<T<<" avgQ="<<(double)sumQ/T<<" totalQ="<<sumQ<<" totalScore="<<score<<"\n";
return 0;
#else
ios::sync_with_stdio(false);
cin.tie(nullptr);
Solver solver;
Timer t;
solver.timer = &t;
while(true){
if(!solver.time_ok()){
// As a last resort, just keep asking any remaining candidates.
// (We still want to finish correctly.)
}
int q = solver.choose_next();
cout << CODES[q].s << "\n" << flush;
// read 30 pairs
array<int,K> hist{};
bool invalid=false;
for(int i=0;i<30;i++){
int h,b;
if(!(cin>>h>>b)) return 0;
if(h==-1 && b==-1) invalid=true;
if(!invalid){
int cid = HB_ID[h][b];
if(cid<0) cid=0;
hist[cid]++;
}
}
if(invalid){
// must terminate immediately
return 0;
}
// If smallest pair has hit=5, then all are (5,0) because sorted.
// We don't have full list, but equivalently hist[50]==30.
if(hist[HB_ID[5][0]]==30){
return 0;
}
solver.apply_response(q,hist);
}
#endif
}