結果

問題 No.5025 Forest Fire
コンテスト
ユーザー bird01
提出日時 2026-10-24 17:20:19
言語 C++23
(gcc 15.3.0 + boost 1.92.0 + ACL)
コンパイル:
g++-15 -O2 -lm -std=c++23 -Wuninitialized -DONLINE_JUDGE -o a.out _filename_
実行:
./a.out
結果
AC  
実行時間 1,933 ms / 2,000 ms
+ 870µs
コード長 9,945 bytes
記録
記録タグの例:
初AC ショートコード 純ショートコード 純主流ショートコード 最速実行時間
コンパイル時間 3,192 ms
コンパイル使用メモリ 360,336 KB
実行使用メモリ 9,924 KB
スコア 140,663,429
最終ジャッジ日時 2026-10-24 17:27:41
合計ジャッジ時間 160,784 ms
ジャッジサーバーID
(参考情報)
judge2_0 / <nil>
純コード判定しない問題か言語
このコードへのチャレンジ
(要ログイン)
ファイルパターン 結果
other AC * 50
権限があれば一括ダウンロードができます

ソースコード

diff #
raw source code

// 3.
#include <bits/stdc++.h>
using namespace std;

// 仕切りは配列中の並び順で生成する
struct Wall {
  int y, x;
  double angle;
};

const int TRIALS = 100;
const int LIMIT_MS = 1950;
const int MAX_CUT = 100;
const double PI = acos(-1.0);

// 消失確率の和を求める。burn_count 指定時は各マスの焼失回数も記録する
// 雷は木が残っているマスから選び、湿った木に落ちた場合は延焼しない
// cntの有無は記録だけを変える
// tree: 初期の木(位置), cut: 伐採有無
// 出力の評価値 = (100試行の焼失本数の合計) / 100

double simulate(const vector<int>& d, const vector<vector<int>>& adj,
                const vector<unsigned char>& cut, mt19937& rng,
                vector<int>* burn_count = nullptr) {
  int NN = (int)d.size();
  vector<int> alive;
  alive.reserve(NN);
  for(int p=0; p<NN; p++) {
    if(d[p] > 0 && !cut[p]) alive.push_back(p);
  }
  if(alive.empty()) return 0.0;

  if(burn_count != nullptr) burn_count->assign(NN, 0);
  uniform_int_distribution<int> rnd10(0, 9);
  uniform_int_distribution<int> rnd_tree(0, (int)alive.size() - 1);
  vector<unsigned char> dry(NN, 0);
  vector<int> q(NN);
  long long sum_burned = 0;

  for(int t=0; t<TRIALS; t++) {
    fill(dry.begin(), dry.end(), 0);
    for(int p : alive) dry[p] = rnd10(rng) < d[p];

    int start = alive[rnd_tree(rng)];
    if(!dry[start]) continue;

    int head = 0, tail = 0;
    q[tail++] = start;
    dry[start] = 0;
    while(head < tail) {
      int p = q[head++];
      if(burn_count != nullptr) (*burn_count)[p]++;
      for(int to : adj[p]) {
        if(!dry[to]) continue;
        dry[to] = 0;
        q[tail++] = to;
      }
    }
    sum_burned += tail;
  }
  return (double)sum_burned / TRIALS;
}

// 3×3内での4近傍BFS。対象マスは未伐採として呼び出す
bool need_cut(int y, int x, int N, const vector<unsigned char>& eligible,
              const vector<unsigned char>& cut) {
  bool vis[3][3] = {};
  int qy[9], qx[9], head = 0, tail = 0;
  vis[1][1] = true;
  qy[tail] = y;
  qx[tail] = x;
  tail++;

  const int dy[4] = {-1, 1, 0, 0};
  const int dx[4] = {0, 0, -1, 1};
  while(head < tail) {
    int cy = qy[head], cx = qx[head];
    head++;
    for(int k=0; k<4; k++) {
      int ny = cy + dy[k], nx = cx + dx[k];
      if(ny<0 || ny>=N || nx<0 || nx>=N) continue;
      if(abs(ny-y)>1 || abs(nx-x)>1) continue;
      int p = ny*N + nx;
      if(!eligible[p] || cut[p]) continue;
      int iy = ny-y+1, ix = nx-x+1;
      if(vis[iy][ix]) continue;
      vis[iy][ix] = true;
      qy[tail] = ny;
      qx[tail] = nx;
      tail++;
    }
  }

  for(int dy=-1; dy<=1; dy++) {
    for(int dx=-1; dx<=1; dx++) {
      if(dy==0 && dx==0) continue;
      int ny = y+dy, nx = x+dx;
      if(ny<0 || ny>=N || nx<0 || nx>=N) continue;
      int p = ny*N + nx;
      if(eligible[p] && !cut[p] && !vis[dy+1][dx+1]) return false;
    }
  }
  return true;
}

// 直線上の候補マスを順番に収集する。既存の仕切り・対象外・盤面外で停止
void collect_ray(int y, int x, double dy, double dx, int N,
                 const vector<unsigned char>& eligible,
                 const vector<unsigned char>& cut, vector<int>& candidates) {
  int sy = (dy>0) - (dy<0);
  int sx = (dx>0) - (dx<0);
  double next_y = sy ? 0.5 / abs(dy) : numeric_limits<double>::infinity();
  double next_x = sx ? 0.5 / abs(dx) : numeric_limits<double>::infinity();
  double step_y = sy ? 1.0 / abs(dy) : numeric_limits<double>::infinity();
  double step_x = sx ? 1.0 / abs(dx) : numeric_limits<double>::infinity();

  while(true) {
    if(next_y < next_x) {
      y += sy;
      next_y += step_y;
    } else {
      x += sx;
      next_x += step_x;
    }
    if(y<0 || y>=N || x<0 || x>=N) break;
    int p = y*N + x;
    if(!eligible[p] || cut[p]) break;
    candidates.push_back(p);
  }
}

// 仕切り情報を生成順に再生し、伐採マスを確定する
vector<unsigned char> build_cut(int N, const vector<unsigned char>& eligible,
                                const vector<Wall>& walls) {
  vector<unsigned char> cut(N*N, 0);
  int count = 0;
  for(const Wall& w : walls) {
    if(count >= MAX_CUT) break;
    int start = w.y*N + w.x;
    if(!eligible[start] || cut[start]) continue;

    // 今回の仕切りの候補を、生成ポイント・成長方向・逆方向の順に列挙
    vector<int> candidates = {start};
    double dy = sin(w.angle), dx = cos(w.angle);
    collect_ray(w.y, w.x, dy, dx, N, eligible, cut, candidates);
    collect_ray(w.y, w.x, -dy, -dx, N, eligible, cut, candidates);

    // 伐採枠内の候補を先に仮伐採する(未判定の候補もBFSに反映)
    int end = min((int)candidates.size(), MAX_CUT-count);
    for(int i=0; i<end; i++) cut[candidates[i]] = 1;

    int i = 0;
    for(; i<end; i++) {
      int p = candidates[i];
      // 対象マスだけを一時的に未伐採にしてBFSを実行
      cut[p] = 0;
      if(need_cut(p/N, p%N, N, eligible, cut)) {
        cut[p] = 1;
        count++;
      } else {
        // 外した候補は未伐採のまま。空いた枠に次の候補を仮伐採
        if(end < (int)candidates.size()) {
          cut[candidates[end]] = 1;
          end++;
        }
      }
      if(count >= MAX_CUT) {
        i++;
        break;
      }
    }

    // 100本到達時に、未判定の仮伐採マスを残さない
    for(; i<end; i++) cut[candidates[i]] = 0;
  }
  return cut;
}

int main() {
  auto start_time = chrono::steady_clock::now();
  ios::sync_with_stdio(false);
  cin.tie(nullptr);

  int N, K;
  cin >> N >> K;
  vector<int> d(N*N, 0);
  for(int i=0; i<N; i++) {
    string s;
    cin >> s;
    for(int j=0; j<N; j++) d[i*N+j] = s[j]-'0';
  }

  vector<vector<int>> adj(N*N);
  const int dy4[4] = {-1,1,0,0};
  const int dx4[4] = {0,0,-1,1};
  for(int y=0; y<N; y++) {
    for(int x=0; x<N; x++) {
      for(int k=0; k<4; k++) {
        int ny = y+dy4[k], nx = x+dx4[k];
        if(ny<0 || ny>=N || nx<0 || nx>=N) continue;
        adj[y*N+x].push_back(ny*N+nx);
      }
    }
  }

  mt19937 rng(random_device{}());
  uniform_real_distribution<double> rnd01(0.0, 1.0);
  uniform_real_distribution<double> rnd_angle(0.0, 2.0*PI);
  vector<unsigned char> no_cut(N*N, 0);

  // 初期盤面の消失確率は一度だけ推定し、以後使い回す
  vector<int> initial_burn;
  simulate(d, adj, no_cut, rng, &initial_burn);
  vector<unsigned char> eligible(N*N, 0);
  vector<int> points;
  for(int p=0; p<N*N; p++) {
    if(d[p]>0 && initial_burn[p]>=25) {
      eligible[p] = 1;
      points.push_back(p);
    }
  }

  // 消失確率0.25以上の木がなければ仕切りを生成できない
  if(points.empty()) {
    cout << 0 << '\n';
    return 0;
  }

  uniform_int_distribution<int> rnd_point(0, (int)points.size()-1);
  auto random_wall = [&]() {
    int p = points[rnd_point(rng)];
    return Wall{p/N, p%N, rnd_angle(rng)};
  };

  // 初期解は仕切り0個
  vector<Wall> current;
  vector<unsigned char> current_cut = no_cut;
  double current_score = simulate(d, adj, current_cut, rng);
  vector<unsigned char> best_cut = current_cut;
  double best_score = current_score;

  while(true) {
    double ms = chrono::duration<double, milli>(
      chrono::steady_clock::now()-start_time).count();
    if(ms >= LIMIT_MS) break;

    // 5種類の近傍を等確率で選択
    int type = uniform_int_distribution<int>(0, 4)(rng);
    if(current.empty() && type != 4) continue;
    vector<Wall> next = current;

    if(type == 0) {
      // 削除・追加
      int idx = uniform_int_distribution<int>(0, (int)next.size()-1)(rng);
      next.erase(next.begin()+idx);
      next.push_back(random_wall());
    } else if(type == 1) {
      // 方向変更
      int idx = uniform_int_distribution<int>(0, (int)next.size()-1)(rng);
      next[idx].angle = rnd_angle(rng);
    } else if(type == 2) {
      // ポイント変更:マンハッタン距離3以内の対象点に移動
      int idx = uniform_int_distribution<int>(0, (int)next.size()-1)(rng);
      vector<int> candidates;
      for(int dy=-3; dy<=3; dy++) {
        for(int dx=-3; dx<=3; dx++) {
          if(abs(dy)+abs(dx)>3 || (dy==0 && dx==0)) continue;
          int ny = next[idx].y+dy, nx = next[idx].x+dx;
          if(ny<0 || ny>=N || nx<0 || nx>=N) continue;
          if(eligible[ny*N+nx]) candidates.push_back(ny*N+nx);
        }
      }
      if(candidates.empty()) continue;
      int p = candidates[uniform_int_distribution<int>(0, (int)candidates.size()-1)(rng)];
      next[idx].y = p/N;
      next[idx].x = p%N;
    } else if(type == 3) {
      // 生成順変更
      int idx = uniform_int_distribution<int>(0, (int)next.size()-1)(rng);
      Wall w = next[idx];
      next.erase(next.begin()+idx);
      int pos = uniform_int_distribution<int>(0, (int)next.size())(rng);
      next.insert(next.begin()+pos, w);
    } else {
      // 単純追加
      next.push_back(random_wall());
    }

    vector<unsigned char> next_cut = build_cut(N, eligible, next);
    double next_score = simulate(d, adj, next_cut, rng);

    // 評価値が小さい方を優先する焼きなまし
    // 指数関数的に開始温度1000から終了温度0.5へ変化
    double progress = min(1.0, ms / LIMIT_MS);
    double temperature = 1000.0 * pow(0.5/1000.0, progress);
    double delta = next_score - current_score;
    if(delta <= 0.0 || rnd01(rng) < exp(-delta/temperature)) {
      current = move(next);
      current_cut = move(next_cut);
      current_score = next_score;
      if(current_score < best_score) {
        best_score = current_score;
        best_cut = current_cut;
      }
    }
  }

  vector<pair<int,int>> answer;
  for(int p=0; p<N*N; p++) {
    if(best_cut[p]) answer.push_back({p/N, p%N});
  }
  cout << answer.size() << '\n';
  for(auto [y,x] : answer) cout << y << ' ' << x << '\n';
  return 0;
}
0