TSG LIVE! 11 プログラミングコンテスト に参加した

たまたま TL に出てきたので参加。 意外と Twitter のおすすめは有能?

何問か気になったので記事にして書きます。

解法ものせるので、解きたい人は先に問題を見るか、慎重にスクロールしてください。

www.hackerrank.com

F: Three Keys Maze

問題概要

$N$ 種類の部屋と $M{}$ 個の通路で構成された迷宮がある。 また $K$ 種類の鍵が存在する。

部屋 $i$ には鍵 $a_{i,1},\dots a_{i,k_{i}}$ の $k_{i}$ 個の鍵がある。

鍵は持ち出せるが、同時に3個しか持てず、鍵を捨てた場合、その鍵は元の部屋に戻る。

通路 $i$ は部屋 $s_{i}$ と $t_{i}$ を結んでいて鍵 $b_{i}$ を持っているとき通過できる。

部屋 $1$ から $N$ までの最短距離はいくらか

$1\leq N\leq 100$

$1\leq M\leq 1000$

$1\leq K\leq 30$

考察

逆に、 $N$ から $1$ に行くことを考える。

このとき、鍵はいつでも拾えるが、部屋にあるゴミ箱にしか捨ててはいけないという感じになる。

鍵を拾うのは、通路を通る直前でよくて、鍵は捨てれるなら捨てちゃっていい。

これをすると、今いる部屋と持っている鍵を状態として見ることで、頂点数 $O(NK^{3})$ 辺の数 $O(K^{3}M)$ の$01$ BFS になる。

ある辺について、その辺を通れる状態数というのは $O(K^{2})$ 程度なので実際辺の数は $O(K^{2}M)$ になる。まあ多分通るやろと適当にやったけど通った

void solve(){
    int N,M,K;
    cin>>N>>M>>K;
    vector<vector<bool>> key(N,vector<bool>(K+1));
    rep(i,0,N){
        int a;
        cin>>a;
        rep(j,0,a){
            int b;
            cin>>b;
            key[i][b]=1;
        }
    }
    vector<vector<pair<int,int>>> G(N);
    rep(i,0,M){
        int a,b,c;
        cin>>a>>b>>c;
        a--,b--;
        G[a].push_back({b,c});
        G[b].push_back({a,c});
    }
    int dp[N][K+1][K+1][K+1];
    rep(i,0,N) rep(j,0,K+1) rep(k,0,K+1) rep(l,0,K+1) dp[i][j][k][l]=-1;
    //vector dp(N,vector(K+1,vector(K+1,vector<int>(K+1,-1))));
    vector<tuple<int,int,int,int>> order;
    order.push_back({N-1,0,0,0});
    dp[N-1][0][0][0]=0;
    vector<int> p,q;
    rep(rp,0,order.size()){
        int a,b,c,d;
        tie(a,b,c,d)=order[rp];
        int cost=dp[a][b][c][d];
        p={b,c,d};
        rep(i,0,3) if(key[a][p[i]]) p[i]=0;
        if(a==0&&vec_max(p)==0){
            cout<<cost<<"\n";
            return;
        }
        for(auto e:G[a]){
            q=p;
            bool ok=0;
            rep(i,0,3){
                if(q[i]==e.second) ok=1;
            }
            rep(i,0,3){
                if(!ok&&q[i]==0) q[i]=e.second,ok=1;
            }
            if(ok){
                sort(all(q));
                if(dp[e.first][q[0]][q[1]][q[2]]==-1){
                    dp[e.first][q[0]][q[1]][q[2]]=cost+1;
                    order.push_back({e.first,q[0],q[1],q[2]});
                }
            }
        }
    }
    cout<<"-1\n";
}

I: Plus||Minus||Zero

長さ $N$ の正整数列 $X$ が与えられるので、$i=1,2,\dots , N$ について、独立に以下の問いに答える。

$X_{i}$ を $0$ に変更する。 要素が $-1,0,1$ のいずれかである長さ $N$ の整数列 $s$ を用いて、 $\sum s_{j}X_{j}$ と表せない正整数のうち、最も最小のものを求める。

$2\leq N\leq 60$

$1\leq X_{i}\leq 20000$

考察

この程度の制約なら bitset 高速化で全ての問題を独立に解くことができる。

$O(N^{3}X/w)$ とか?

void solve(){
    const int M=3000000,C=M/2;
    int N;
    cin>>N;
    vector<int> X(N);
    rep(i,0,N) cin>>X[i];
    rep(i,0,N){
        bitset<M> dp;
        dp[C]=1;
        rep(j,0,N) if(i!=j) dp=((dp)|(dp<<X[j])|(dp>>X[j]));
        int ind=1;
        while(dp[ind+C]) ind++;
        cout<<ind<<"\n";
    }
}

J: Infinite Pokaiko Theorem

英小文字からなる文字列 $S$ と整数 $N$ が与えられます。

文字列 $T$ について、スコアを以下のようにします。

  • $T$ の連続部分列で、 $S$ であるものをひとつ見つけ、その区間の $|S|$ 個の文字を全て # に変えるという操作を $T$ に対して行うことができる回数の最大値

文字列 $T$ を長さ $N$ の英小文字からなる文字列から一様ランダムにとったとき、スコアの期待値を $\pmod{998244353}$

$1\leq |S|\leq 2000$

$1\leq N\leq 10^{18}$

考察

$T$ から $S$ を取る操作というのは前から貪欲でいい。

こういう問題は後ろの何文字が $S$ の prefix と一致しているかというのを状態として持って dp すれば良くて、 高々 $2$ 次の多項式を持って行列累乗すれば $O(|S|^{3}\log(N))$ で解ける。

これくらいシンプルな形の行列累乗の形は BMBM 法が適用できるらしいので $O(|S|^{2}\log(|S|)\log(N))$ で解ける。これが想定解らしい。

自分は $O(|S|\log(|S|)\log(N))$ で解いた。

まず、以下の条件を満たす多項式 $f(x)$ を求める。

  • $[x^{a}](f(x))=$ 長さ $a$ の文字列をランダムにとったとき、 連続部分列で $S$ が含まれる箇所が一番最後のみである文字列が出現する期待値

$a\lt |S|$ については $[x^{a}](f(x))=0$ となる。

$|S\leq a$ については、$\frac{1}{26^{|S|}}$ から一番最初に $S$ が現れるのが長さ $k$ になったタイミングであるものの全ての $|S|\leq k\lt a$ に対する総和を引けば良い。

一番最後が $S$ である長さ $k$ の文字列 $U$ にランダムに $|T|-k$ 文字足すことで最後が $S$ になる確率を求めたい。

$|S|\leq |T|-k$ のときの確率は $\frac{1}{26^{|S|}}$。

$|T|-k\lt |S|$ のときは足す文字は $S$ の後ろ $|T|-k$ 文字になる。その文字を足して末尾を $S$ にできるなら確率は $\frac{1}{26^{|T|-k}}$ となる。

これを式で表すと以下のようになる。

$[x^{a}](f(x))=\frac{1}{26^{|S|}}-[x^{a}](\frac{x^{|S|}f(x))}{26^{|S|}(1-x)})-[x^{a}](g(x)f(x))$

$g(x)$ は定数項が $0$ で $x^{i}(1\leq i\lt N)$ の項は $S$ の $(N-i)$ 文字の接頭辞と接尾辞が同じなら $\frac{1}{26^{i}}$ そうでないなら $0$ の係数がつく。これは Zalgo を用いて $O(|S|)$ で求められる。

以上より、 $f(x)$ は以下を満たす。

$f(x)=\frac{x^{|S|}(1-f(x))}{26^{|S|}(1-x)}-g(x)f(x)$

$\frac{f(x)}{1-f(x)}=\frac{x^{|S|}}{26^{|S|}(1-x)(1+g(x))}$

実はこの問題の答えは $\frac{f(x)}{(1-f(x))(1-x)}$ なので、あとはボスタン森法を使えば良い。

この問題の答えは $\frac{f(x)}{(1-f(x))(1-x)}$になるのは、 $[x^{a}]\frac{f(x)}{1-f(x)}$ が $T$ から貪欲に $S$ をとったときに $a$ 文字目が末尾のものを取る確率になっているので、それの累積和を求めればスコアの期待値が求まるから。

hackerrank エアプすぎて、 c++ を選んで提出していたが、 c++20 を使うには c++20 を別で選ばなきゃいけなくて、それを知らなかったせいで ACL の畳み込みのライブラリがコンパイルできなくて通らなかった。

畳み込みを別のものを使って、コンテスト後にAC

この実装は Z_algo を使ってないため、追加で $O(|S|^{2})$ かかるが、そこを置き換えればより大きい制約でも解くことができる。

void solve(){
    ll N;
    string S;
    cin>>N>>S;
    int L=S.size();
    const ll M=26;
    ll rev=mod+1;
    while(rev%M) rev+=mod;
    rev/=M;
    vector<ll> p(L+1,1);
    rep(i,0,L) p[i+1]=(p[i]*rev)%mod;
    vector<ll> g(L);
    rep(i,1,L){
        bool ok=1;
        rep(j,i,L) if(S[j]!=S[j-i]) ok=0;
        if(ok) g[i]=p[i];
    }
    vector<ll> U={1,-1};
    g[0]=1;
    auto tmp=convolution(g,U);

    vector<ll> A(L+1);
    A[L]=p[L];
    auto B=tmp;
    B=convolution(B,U);
    auto ans=po167::boston_mori(A,B,N);
    cout<<ans<<"\n";
}

似たような問題が nowcoder の multiuni で出てた。

ac.nowcoder.com

問題概要としては正整数 $N,K$ と01文字列 $T$ が与えられるので、長さ $N$ の01文字列で連続部分列として $T$ が現れる回数がちょうど $K$ 回なものの数を mod 998 で求める問題。今回は取る箇所が重なっていても良い

これも一番後ろに $T$ が出てくるものの数を表す多項式を組んで一工夫すれば解ける