競技プログラミング日記

主に AtCoder の記事です

AtCoder Beginner Contest 345D問題

ABC345D

解法

回転と裏返しも良いと書いてあるが,裏返しは意味がない. 回転が高々 \(2^{N}\) 通り, 埋める順番が高々 \(N!\) 通り. \(N \leq 7\) なので,遷移に時間が多少掛かっても間に合う.
判定問題を試すときは,取りこぼしさえ無ければ十分. 数え上げ問題の場合は,取りこぼしがないだけでなく,重複も取り除かないといけない. 今回は判定問題なので,重複しても良い. 左上のマスから全探索して,長方形の左上を置くことにする. これを,並べ替え \(N!\) と回転 \(2^{N}\) に対して行う.

実装

DFSで実装した. next_permutation と bitmask でも可能.

使っている記号,マクロ等 "https://ecsmtlir.hatenablog.com/entry/2022/12/23/131925"

int main() {
  ll n,h,w;
  cin >> n >> h >> w;
  vector<pll> ab(n);
  rep(i,n){
    cin >> ab[i].first >> ab[i].second;
  }

  using B = vector<string>; // board

  auto dfs = [&](auto dfs, B s, vector<bool>& used) -> bool {
    ll sx = -1, sy = -1;
    [&](){
      rep(x,h) rep(y,w) if(s[x][y] == '.'){
        sx = x;
        sy = y;
        // break; // 一つしか for を抜けない
        return ;
      }
    }();
   
    if(sx == -1) return true;

    bool any = true;
    rep(i,used.size()) any &= used[i];
    if(any) return false;

    // rectangle
    rep(ri,n) if(!used[ri]){ // used 判定
      auto [a,b] = ab[ri];
      rep(flip, 2){
        swap(a,b);

        B ns = s;
        bool ok = [&]() -> bool {
          if(sx + a > h) return false;
          if(sy + b > w) return false;

          rep(xi, a) rep(yi, b){
            ll nx = sx + xi;
            ll ny = sy + yi;
            if(ns[nx][ny] != '.') return false;
            ns[nx][ny] = '#';
          }

          return true;
        }();

        if(ok){
          used[ri] = true;
          if(dfs(dfs, ns, used)) return true;
          used[ri] = false;
        }
      }
    }

    return false;
  };
 
  B s(h, string(w,'.'));
  vector<bool> used(n);

  yesno(dfs(dfs, s, used));


  return 0;
}

AtCoder Beginner Contest 345C問題

\(\def \set #1#2{\{ #1 \ \vert \ #2 \}}\)

ABC345C

解法
\(X := S\) から 1 swap で作れる string 全体の集合

とする.答えは \(|X|\).

  • \(S \in X \iff\) for some \(i\) and \(j\) \(\in N\) such that \(i < j\) and \(S_{i} = S_{j}\)
  • \begin{align} X - \{S\} &= \set{T}{for\,\,some\,\,i\,\,and\,\,j\,\,such\,\,that\,\,i\,\,<\,\,j\,\,and\,\,S_{i}\,\,\neq\,\,S_{j} and\,\,T\,\,is\,\,the\,\,string\,\,swap\,\,i\,\,and\,\,j\,\,on\,\,S}\,\,\\ &\cong \set{(i,j)\,\,\in\,\,N^{2}}{for\,\,some\,\,i\,\,and\,\,j\,\,such\,\,that\,\,i\,\,<\,\,j\,\,and\,\,S_{i}\,\,\neq\,\,S_{j}}\,\,\\ \end{align}

基本は \(X - \{S\} \) を数えればよく, \(S \in X\) のときだけ +1 すればよい.

使っている記号,マクロ等 "https://ecsmtlir.hatenablog.com/entry/2022/12/23/131925"


int main() {
  string s; cin >> s;
  ll n = s.length();

  vll cnt(26);
  bool f = false;
  rep(i,n){
    cnt[s[i]-'a']++;
    if(cnt[s[i]-'a'] >= 2) f = true;
  }

  ll ans = f;
  ans += nC2(n);
  rep(i,26) ans -= nC2(cnt[i]);
  cout << ans << endl;


  return 0;
}

AtCoder Beginner Contest 344D問題

ABC344D

解法

DP. 状態を,\(T\) の \([0,j)\) まで一致させたとして \(j\) をもつ. \(S_{i}\) を使えるかどうかは, \(T_{[0,j)} + S_{i} = T_{[0,j+|S_{i}|)}\) と同値.

注意

substr は,範囲外にならない様に.

使っている記号,マクロ等 "https://ecsmtlir.hatenablog.com/entry/2022/12/23/131925"

int main() {
  string t; cin >> t;
  ll n; cin >> n;
  vector<vector<string>> s(n);
  rep(i,n){
    ll a; cin >> a;
    s[i].resize(a);
    cinv(s[i]);
  }

  // O(n * t.size() * sum_{i \in n and str \in s[i]} |str|)
  ll inf = n+1;
  vll dp(t.size()+1, inf);
  dp[0] = 0;
int main() {
  string t; cin >> t;
  ll n; cin >> n;
  vector<vector<string>> s(n);
  rep(i,n){
    ll a; cin >> a;
    s[i].resize(a);
    cinv(s[i]);
  }

  // O(n * t.size() * sum_{i \in n and str \in s[i]} |str|)
  ll inf = n+1;
  vll dp(t.size()+1, inf);
  dp[0] = 0;
  rep(i,n) {
    drep(ti, t.size()+1) {
      for(auto str: s[i]){
        if(t.substr(ti, str.length()) == str){
          chmin(dp[ti + str.length()], dp[ti] + 1);
        }
      }
    }
  }

  ll ans = dp[t.size()];
  if(ans >= inf) ans = -1;
  cout << ans << endl;


  return 0;
}
    drep(ti, t.size()+1) {
      for(auto str: s[i]){
        if(t.substr(ti, str.length()) == str){
          chmin(dp[ti + str.length()], dp[ti] + 1);
        }
      }
    }
  }

  ll ans = dp[t.size()];
  if(ans >= inf) ans = -1;
  cout << ans << endl;


  return 0;
}

PAST12K問題

PAST12K

解法
Easy

削除のみの場合. クエリを先読みして,逆に処理すれば,union-find で解ける.

Normal

追加と削除の場合. ただし,追加する辺が 10個以下.
この場合もベースになるのは Easy版で,クエリ先読みと逆順処理をする. もとのクエリで追加する辺(逆順処理では削除する辺)は少ないので, その度に union-find を構成しなおしても間に合う.

実装

Union-find を再構成するために, 逆順で処理したときに,追加した辺を記録しておく.

使っている記号,マクロ等 "https://ecsmtlir.hatenablog.com/entry/2022/12/23/131925"

int main() {
  ll n,m;
  cin >> n >> m;
  UnionFind<ll> uf;
  set<pll> es;
  rep(i,m){
    ll a,b; cin >> a >> b;
    --a, --b;
    es.insert({a,b});
  }

  ll q; cin >> q;
  vector<tuple<ll,ll,ll>> qs;
  rep(qi,q){
    ll t,x,y; cin >> t >> x >> y;
    --x,--y;
    qs.emplace_back(t,x,y);

    if(t == 1){
      es.insert({x,y});
    }else if(t == 2){
      assert(es.find({x,y}) != es.end());
      es.erase({x,y});
    }
  }
  reverse(all(qs));

  auto create_uf = [&](void) -> UnionFind<ll> {
    UnionFind<ll> ret(n);
    for(auto [a,b]: es){
      ret.merge(a,b);
    }
    return ret;
  };
 
  uf = create_uf();

  vector<string> ans;
  for(auto [t,x,y]: qs){
    if(t == 1){
      es.erase({x,y});
      uf = create_uf();
    }else if(t == 2){
      es.insert({x,y});
      uf.merge(x,y);
    }else{
      ans.push_back(uf.same(x,y) ? "Yes" : "No");
    }
  }
  reverse(all(ans)); // rem: reverse

  for(auto s: ans){
    cout << s << endl;
  }


  return 0;
}

PAST12H問題

PAST12H

解法

貨幣の枚数を考える. 組(金貨,銀貨,銅貨) が辞書順で最大になるのがベスト. 銅貨の残り枚数を状態にもって DP すれば O(NX).

使っている記号,マクロ等 "https://ecsmtlir.hatenablog.com/entry/2022/12/23/131925"

pll operator + (pll a, pll b){
  return make_pair(a.first + b.first, a.second + b.second);
}

int main() {
  ll n,x;
  cin >> n >> x;
  vll a(n), b(n), c(n);
  rep(i,n){
    cin >> a[i] >> b[i] >> c[i];
  }

  // r: remainder bronze
  vector<pll> dp(x+1, {-1, -1});
  dp[x] = {0,(ll)1e9};
  rep(i,n) {
    rep(br,x+1){
      if(br - b[i] >= 0) chmax(dp[br - b[i]], dp[br] + make_pair(c[i], -a[i]));
    }
  }

  tuple<ll,ll,ll> ans(-1,-1,-1);
  rep(br,x+1){ // get bronze
    auto [g,s] = dp[br]; // get gold, get silver
    chmax(ans, make_tuple(g, s, br));
  }
  auto [p,q,r] = ans;
  cout << p << " " << q << " " << r << endl;


  return 0;
}

PAST12G問題

PAST12G

解法

何を全探索するか考える. 素直に \(T\) を全探索するのでは \(TLE\) . \(T\) は全探索できないが,\(T\) の \(?\) の位置は,最大で \({}_{10}C_{5} = 252\)なので十分間に合う.
まず \(S := S_{i}\) を一つ固定して考える. \(?\) の位置だけ固定したとき, \(S\) から \(T\) を作れる条件を考える. \(j \in T.size()\) において,

  • \(T_{j} = '?'\) なら,\(S_{j}\) は何でもよい.
  • \(T_{j} \neq '?'\) なら,\(S_{j} = T_{j}\) が必要.

よって, 全ての \(j \in |T|\) with \(T_{j} = '?'\) と なる \(j\) に対して \(S_{j}\) を \('?'\) で置き換えて, \(S = T\) となることが,\(S\)から \(T\) を作れる必要条件. これが十分条件であることは明らか.
次に \(i \in N\) を動かして考えてみるが, これは全体で \(O({}_{L}C_{L/2} N)\) となる. ただし,\(S_{i}\) の一部を \(?\) で置き換えた文字列の個数をカウントするときに map を使っているので,オーダーに \(max_{i \in N} (|S|) log N\) が掛かる.

実装

\(?\) の取り方は bitmask を用いると簡単. \(?\) の選び方 \(\in 2^{L}\) のうち,popcount が \(K\) の物だけ使えばよい.

使っている記号,マクロ等 "https://ecsmtlir.hatenablog.com/entry/2022/12/23/131925"


int main() {
  ll n,l,k;
  cin >> n >> l >> k;
  vector<string> s(n); cinv(s);

  ll ans = 0;
  rep(msk, 1LL << l) if(pcntll(msk) == k){
    vector<string> ns = s;
    rep(i,n) rep(j,l) if(msk >> j & 1) ns[i][j] = '?';

    map<string,ll> cnt;
    rep(i,n) cnt[ns[i]]++;

    for(auto [_,c]: cnt){
      chmax(ans, c);
    }

  }
  cout << ans << endl;

  return 0;
}

PAST15 J問題

PAST15J

問題の言い換え

ダメージの累計が最小になる様に手裏剣を使う.

思考の流れ

いくつか思いつくことがある.

  • まず暫定的な答えを出す.これは簡単に求まるというのが大事. 手裏剣を 0枚使ったときの求めて,その答えを修正していく. 手裏剣 1枚でどれくらい得をするのかを考える.
  • 敵が 1体のときに,\(x = 0, 1\) で場合分けして考える.
  • 敵が \(N\) 体で,全て \(x = 0\) の場合を考える. \(x = 1\) の場合は,手裏剣を投げることで \(x=0\) に帰着できるから.

それぞれについて詳しく見ていく.
敵が 1体のとする.手裏剣を \(u\) 枚使ったとする.

  • \(x = 0\) なら \(b-u-1\) 回攻撃を食らう. これは \(u=0\), \(u > 0\) による場合分けは不要.
  • \(x = 1\) なら,手裏剣を投げるかどうかで場合分け.
    • \(u = 0\) のときは,\(b - u\) 回攻撃をくらう.
    • \(u > 0\) のとき
      • \(b > 1\) のとき,先手をとれるおかげで \(b-u-1\) 回攻撃をくらう.
      • \(b = 1\) のとき,0回攻撃を食らう.

まとめると, \(x = 1\)のときは基本的に手裏剣 1枚目だけダメージが \(2a\) 得するが, \(b = 1\) のときは \(a\) 得する. \(1\) 枚投げれば,\(x = 0\) に帰着できる. \(x = 0\)のときは,1枚につき \(a\) 得する.
また,手裏剣を 0枚使ったときの暫定的な答えはすぐに求まる.

注意

\(x = 1\) かつ \(b = 1\) のときは,手裏剣を \(1\)枚投げても \(a\) しか得をしない.

使っている記号,マクロ等 "https://ecsmtlir.hatenablog.com/entry/2022/12/23/131925"

int main() {
  ll n,m;
  cin >> n >> m;

  using T = tuple<ll,ll,ll,ll>;
  priority_queue<T> ts;
  ll dmg = 0;

  rep(i,n){
    ll a,b,x; cin >> a >> b >> x;
   
    ll c = (x+1) * a;
    if(b == 1) c = a; // b == 1 && x == 1
    ts.push({c, a, b, x});
    dmg += (b-(!x))*a; // 手裏剣を使わなかったときのダメージ
  }


  // 手裏剣を上手に配分してダメージを減らす
  while(ts.size() && m > 0){
    auto [c, a, b, x] = ts.top(); ts.pop();

    if(x == 1){
      dmg -= c;
      m--;
      b--;

      if(b > 0) ts.push({a,a,b,0}); // x = 1 は 1枚目の手裏剣だけ特別.2枚目以降は x = 0 に帰着.
    }else{
      ll use = min(m, b-1);
      m -= use;
      dmg -= use * a; // 手裏剣を使ってどれだけ得するか
    }
  }

  cout << dmg << endl;

  return 0;
}