#include using namespace std; // Heuristic solver for the N=50, K=100 stochastic forest-fire problem. // C++17. All objective values below omit the constant divisor N*N. #ifndef GREEDY_S #define GREEDY_S 32 #endif #ifndef GREEDY_END #define GREEDY_END 1.20 #endif using ll = long long; using Clock = chrono::steady_clock; struct RNG { uint64_t x; explicit RNG(uint64_t s=1234567890123456789ULL) : x(s) {} inline uint64_t next() { uint64_t z = (x += 0x9e3779b97f4a7c15ULL); z = (z ^ (z >> 30)) * 0xbf58476d1ce4e5b9ULL; z = (z ^ (z >> 27)) * 0x94d049bb133111ebULL; return z ^ (z >> 31); } inline int integer(int n) { return int(next() % uint64_t(n)); } inline double real() { return (next() >> 11) * 0x1.0p-53; } }; struct Solver { int N, K, V, eligible=0; vector d, trees; vector> nbr; vector deg; RNG rng; Clock::time_point start; double limit = 1.57; static constexpr int MAXS=384; vector> samples; // Tarjan DFS scratch arrays. vector tin, low, sub, sepSum, sepSq, visitOrder; int timer=0; const vector *curCut=nullptr, *curDry=nullptr; vector seen, compId, compSizes; int stamp=0; vector queue_buf; Solver(int n, int k, vector dd) : N(n), K(k), V(n*n), d(move(dd)), nbr(V), deg(V), rng(0xa920a6b592391fdeULL), start(Clock::now()), tin(V), low(V), sub(V), sepSum(V), sepSq(V), seen(V,0), compId(V,0), queue_buf(V) { for(int v=0; v0) trees.push_back(v); int i=v/N,j=v%N; if(i>0) nbr[v][deg[v]++]=v-N; if(i+10) nbr[v][deg[v]++]=v-1; if(j+1(V)); for(int s=0; s0 && rng.integer(10)(Clock::now()-start).count(); } inline bool timeLeft(double margin=0.0) const { return elapsed()+margin < limit; } // Computes sum_C |C|^2 on one fixed sample, using a DFS flood fill. inline ll evalOne(const vector &cut, const vector &dry) { ++stamp; ll ans=0; for(int v=0;v &cut, int begin, int cnt) { ll ans=0; for(int s=begin;s=tin[u]) { sepSum[u]+=sub[v]; sepSq[u]+=sub[v]*sub[v]; } } else if(v!=p) low[u]=min(low[u],tin[v]); } } // O(S*(V+edges)) all-vertices removal marginal gains. void removalGains(const vector &cut,int S,vector&gain) { fill(gain.begin(),gain.end(),0LL); for(int s=0;s=0) continue; int first=(int)visitOrder.size(); dfsTarjan(r,-1); int sz=(int)visitOrder.size()-first; int sq=sz*sz; for(int k=first;k<(int)visitOrder.size();k++) { int u=visitOrder[k]; int rest=sz-1-sepSum[u]; gain[u]+=(ll)sq-sepSq[u]-rest*rest; } } } } inline int countCut(const vector&a) { int n=0; for(uint8_t c:a) n+=c; return n; } // Greedily complete an existing (possibly empty) solution. // Has a deterministic per-run time cap; safely returns a valid partial solution. void greedyFill(vector&cut, int S, double endTime) { vector gain(V); int selected=countCut(cut), iterations=0; double blockStart=elapsed(); int currentS=S; while(selectedbestG) { best=v; bestG=gain[v]; } if(best<0) break; cut[best]=1; ++selected; ++iterations; if((iterations&3)==0 && selected0.85*budget && currentS>12) { int proposed=(int)(currentS*(0.83*budget)/ max(1e-9,stepCost*(K-selected))); currentS=max(12,min(currentS,proposed)); } blockStart=now; } } } // A full left-right or top-bottom blocking path; 8-connected blocked // path separates 4-connected open cells. DP permits |delta row|<=1. vector barrier(bool horizontal,int target,int variation) { const int INF=1000000000; vector> prev(N); vector cost(N*N,INF); auto at=[&](int along,int across)->int { return horizontal ? across*N+along : along*N+across; }; for(int across=0;across=N) continue; int val=cost[(col-1)*N+pr]; if(val path; int row=bestrow; for(int col=N-1;col>=0;col--) { path.push_back(at(col,row)); row=prev[col][row]; } return path; } vector pathCut(const vector&a,const vector&b={}) { vector result(V,0); for(int v:a) if(d[v])result[v]=1; for(int v:b) if(d[v])result[v]=1; return result; } void addHelpfulFill(vector&cut,int cntSample) { // Short greedy fill is useful especially where water saves barrier budget. if(countCut(cut)>=K) return; vector gain(V); for(int iter=countCut(cut);iterbestVal) best=v,bestVal=gain[v]; if(best<0) break; cut[best]=1; } } // Cost of restoring a previously felled tree. Components and sizes are // computed once per sample; each cut vertex then needs <=4 neighbor probes. void restorationCosts(const vector&cut,int S,vector&cost) { fill(cost.begin(),cost.end(),0LL); vector cutList=collectCut(cut); for(int s=0;s=0) continue; int cid=(int)compSizes.size(); int head=0,tail=1; queue_buf[0]=v;compId[v]=cid; while(head cut; ll value=LLONG_MAX; }; vector collectCut(const vector&cut) { vector v; for(int u:trees)if(cut[u])v.push_back(u); return v; } // Score on stronger, independent validation samples to reduce overfitting. ll validation(const vector&cut) { return eval(cut,128,192); } void normalize(vector&cut) { int have=countCut(cut); if(have>K) { vector used=collectCut(cut); while(have>K) { int v=used[rng.integer((int)used.size())]; if(cut[v]) {cut[v]=0;--have;} } } if(have g(V); removalGains(cut,24,g); vector> cand; for(int v:trees) if(!cut[v]) cand.emplace_back(g[v],v); sort(cand.begin(),cand.end(),greater>()); for(auto [value,v]:cand) { if(have==K)break; cut[v]=1;have++; } } } // Pick a swap size 1..7 from a time-dependent smooth distribution. // Larger changes are common at the beginning; 1-point refinements dominate // later, but every allowed swap size retains nonzero probability. int sampleSwapSize(double progress) { int maxk=min({7,K,eligible-K}); if(maxk<=0) return 0; double lambda=0.95+1.40*progress; double weights[8], sum=0; for(int k=1;k<=maxk;k++) { weights[k]=0.97*exp(-lambda*(k-1))+0.03; sum+=weights[k]; } double roll=rng.real()*sum; for(int k=1;k<=maxk;k++) { roll-=weights[k]; if(roll<0) return k; } return maxk; } // Exactly k distinct old cuts are replaced by k distinct previously // uncut trees. Local/clustered and priority-guided choices are mixed. bool mutateSwapRange(vector&x, const vector&base, const vector&cutVertices, int k, const vector&lowRestore, const vector&highGain) { if(k<1 || (int)cutVertices.size()=2 && rng.integer(100)<47); bool guide=(!lowRestore.empty() && rng.integer(100)<(k==1?67:45)); int clusterRadius=2+(k+1)/2; vector removed; removed.reserve(k); for(int i=0;i1. if(b<0 && rng.integer(100)<(k>=2?75:57)) { int radius=rng.integer(100)<75?2:6; int r0=a/N,c0=a%N; for(int tries=0;tries<28;tries++) { int r=r0+rng.integer(2*radius+1)-radius; int c=c0+rng.integer(2*radius+1)-radius; if(r<0||r>=N||c<0||c>=N)continue; int v=r*N+c; if(d[v]>0 && !base[v] && !x[v]) {b=v;break;} } } if(b<0 && !highGain.empty() && rng.integer(100)<65) { int cap=min((int)highGain.size(),max(80,k*16)); for(int tries=0;tries<20;tries++) { int v=highGain[rng.integer(cap)]; if(!base[v] && !x[v]) {b=v;break;} } } if(b<0) { for(int tries=0;tries<30;tries++) { int v=trees[rng.integer(eligible)]; if(!base[v] && !x[v]) {b=v;break;} } } if(b<0) for(int v:trees) if(!base[v] && !x[v]) {b=v;break;} if(b<0)return false; x[b]=1; } return true; } // Shift structured separators; always preserves the number of cuts. bool mutate(vector &x, const vector &base, int type, const vector&cutVertices) { x=base; if(cutVertices.empty()) return false; if(type==2) { // shift all cuts on one row/column to an adjacent row/column bool horizontal=rng.integer(2); int line=-1; // Choose one of the most populated lines. vector> ls; for(int z=0;z=5)ls.push_back({n,z}); } if(ls.empty())return false; sort(ls.begin(),ls.end(),greater>()); line=ls[rng.integer(min((int)ls.size(),4))].second; int off=rng.integer(2)*2-1; if(line+off<0||line+off>=N)return false; vector removed,added; for(int w=0;w=N||cc<0||cc>=N)return false; vector group{seed}; vector mark(V,0);mark[seed]=1; int len=3+rng.integer(10); for(int idx=0;idx<(int)group.size() && (int)group.size()=N||c<0||c>=N)continue; int v=r*N+c; if(v==u||!base[v]||mark[v])continue; mark[v]=1;group.push_back(v); if((int)group.size()==len)break; } } vector from,to; for(int u:group) { int r=u/N+dr[move],c=u%N+dc[move]; if(r<0||r>=N||c<0||c>=N)continue; int v=r*N+c; if(d[v] && !base[v]) {from.push_back(u);to.push_back(v);} } if(from.size()<2)return false; for(int u:from)x[u]=0; for(int u:to)x[u]=1; return x!=base; } vector run() { if(K==0)return vector(V,0); vector best(V,0); ll bestVal=LLONG_MAX; const int VALID_START=128, VALID_SIZE=192; auto consider=[&](const vector&x) { ll v=eval(x,VALID_START,VALID_SIZE); if(v middle(V,0); for(int v:trees) if(v/N==N/2 || v%N==N/2)middle[v]=1; normalize(middle); consider(middle); } // Build a full, independent stochastic greedy solution before the // global candidates. This avoids skipping greedy on slow hardware. { vector cut(V,0); greedyFill(cut, GREEDY_S, min(0.96,elapsed()+0.90)); #ifdef PROFILE cerr << "greedy selected " << countCut(cut) << " at " << elapsed() << "\n"; #endif if(countCut(cut)> horizontal,vertical; for(int t: {13,17,20,23,25,27,30,33,36}) { horizontal.push_back(barrier(true,t,0)); vertical.push_back(barrier(false,t,0)); } vector>> top; for(int h=0;h<(int)horizontal.size();h++) { for(int v=0;v<(int)vertical.size();v++) { if(elapsed()>1.25)break; vector cut=pathCut(horizontal[h],vertical[v]); if(countCut(cut)>K)continue; top.push_back({eval(cut,0,6),move(cut)}); } } for(int a=0;a<(int)horizontal.size();a++) for(int b=a+1;b<(int)horizontal.size();b++) { if(elapsed()>1.25)break; auto cut=pathCut(horizontal[a],horizontal[b]); if(countCut(cut)<=K) top.push_back({eval(cut,0,6),move(cut)}); } for(int a=0;a<(int)vertical.size();a++) for(int b=a+1;b<(int)vertical.size();b++) { if(elapsed()>1.25)break; auto cut=pathCut(vertical[a],vertical[b]); if(countCut(cut)<=K) top.push_back({eval(cut,0,6),move(cut)}); } sort(top.begin(),top.end(),[](auto&a,auto&b){return a.first12)top.resize(12); for(auto &item:top) { if(elapsed()>1.33)break; item.first=eval(item.second,0,48); } sort(top.begin(),top.end(),[](auto&a,auto&b){return a.first1.36)break; auto cut=top[i].second; if(countCut(cut)1.42)return best; vector cur=best; const int SA_S=64; ll curVal=eval(cur,0,SA_S); vector candidate; const double annealStart=elapsed(), annealEnd=limit-0.07; if(annealEnd>annealStart+0.03) { double duration=annealEnd-annealStart; double totalScale=double(curVal)/SA_S; double T0=max(1.0,totalScale*0.006); double T1=max(0.2,totalScale*0.000003); vector used=collectCut(cur); int iter=0; int lastRefresh=-1000, acceptCount=0; ll saBest=curVal; vector lowestRestore,highestGain; vector restore(V), gain(V); auto refreshRanking=[&]() { const int S=24; restorationCosts(cur,S,restore); removalGains(cur,S,gain); lowestRestore=collectCut(cur); sort(lowestRestore.begin(),lowestRestore.end(), [&](int a,int b){return restore[a]gain[b];}); if(highestGain.size()>150)highestGain.resize(150); lastRefresh=acceptCount; }; while(elapsed()=16) && elapsed()+0.15>N>>K))return 0; vector d(N*N); for(int&i:d)cin>>i; Solver solver(N,K,move(d)); auto cut=solver.run(); vector positions; for(int v=0;v