CS Academy Ball Sampling

リンク : https://csacademy.com/contest/archive/task/ball-sampling/

問題概要

N色のボールがあり、箱にそれぞれA_i個入っている。同様に確からしいとき、すべての色のボールを一回以上取り出す期待値を求めよ。

解説

いわゆる、コンプリート問題と言われる、期待値と言えばの典型である。tsutajさんの下のスライドにとてもよくまとめられている。
http://compro.tsutajiro.com/archive/180220_probability_dp.pdf
dp[mask]を1回でも取った色の情報をmaskとして、maskから全色集めるまでの期待値とする。最終的な答えはdp[0]となる。

LL n, a[1100], sum;
double dp[1 << 20];
int main() {
    cin >> n;
    rep(i, n) {
        cin >> a[i];
        sum += a[i];
    }
    Rrep(mask, (1LL << n)) {
        LL cur = 0;
        rep(i, n)if ((mask&(1LL << i)) == 0)cur += a[i];
        double p = sum;
        p /= cur;
        rep(i, n) {
            if ((mask&(1LL << i)) == 0) {
                double q = a[i];
                q /= cur;
                dp[mask] += (dp[mask | (1LL << i)] + p) * q;
            }
        }
    }
    printf("%.15f\n", dp[0]);
    return 0;
}

CS Academy Amusement Park

リンク : https://csacademy.com/contest/archive/task/amusement-park/statement/

問題概要

N*Mの行列Aが与えられる。このA[i][j]は座標(i,j)からA[i][j]の確率で座標(i,j+1)に進み、それ以外は座標(i+1,j+1)に進む。このとき、iがNになるときの移動回数の期待値を求めよ。

解説

期待値周りの計算がガッチガチの数学なので、公式の解説を読んだほうがわかりやすいかも。dp[i][j]を今の座標からNになるまでの期待値とする。
dp[0][N]=dp[0][N-1]×A[0][N-1]
dp[0][N-1]=dp[0][N-2]×A[0][N-2]......
dp[0][1]=dp[0][0]×A[0][0]
dp[0][0]=dp[0][N]×A[0][N]
となり、円のように循環になる。なので、dp[0][0]の値が求められると、すべて求められることになる。また、x=dp[0][0]とおくと、後ろから求めることにより、x=a+bxの形になるため、xが求められる。
次にdpiについて考えると、dp[i][N]=dp[i][N-1]×A[i][N-1]+dp[i-1][N-1]×(1-A[i][N-1])......となり、これもまた先ほどと同じように求められる。

int T, A;
double a[1100][1100],dp[1100][1100];
int main() {
    cin >> T >> A;
    rep(i, T)rep(j, A) {
        cin >> a[i][j];
        a[i][j] /= 100;
    }
    for (int i = 0; i<T; i++) {
        double sum1 = 1, sum2 = 1 + (i - 1 >= 0 ? dp[i - 1][0] * (1 - a[i][A - 1]) : 0);
        for (int j = A - 1; j >= 0; j--) {
            sum1 *= (a[i][j]);
            if (j != A - 1)sum2 = (1 + sum2 * (a[i][j]) + (i-1>=0?dp[i - 1][(j+1)%A] * (1 - a[i][j]):0));
        }
        dp[i][0] = sum2 / (1-sum1);
        for (int j = A-1; j >=0; j--) {
            dp[i][j] = 1.0 + dp[i][(j+1)%A] * a[i][j] + (i - 1 >= 0 ? dp[i - 1][(j + 1) % A] * (1 - a[i][j]) : 0);
        }
    }
    printf("%.15f\n",dp[T-1][0]);
    return 0;
}

CS Academy City Attractions

リンク : https://csacademy.com/contest/archive/task/city-attractions/statement/

問題概要

N頂点の木が存在する。それぞれの頂点に美しさA[i]として入力される。d(x,y):=頂点xと頂点yの距離とする。頂点xにいるとき、次に進む場所はA[y]-d(x,y)が最大になる都市である。K回進んだとき、最後の頂点はいくつか。

解説

すぐに全方位木dpっぽいなとは思ったが、MLEがつらすぎた... 全方位木dpとは簡単に説明すると、全頂点を根として一回探索したいときにO(N2)ではなく、O(N)でできる考え方である。一度全方位木dpをして、頂点iにいるときに次に進む場所を計算しておく。これができれば、あとは頂点1から順番に見ていってサイクルが必ず存在するためKをサイクルの大きさで割った余りにして、また前から見ていけばO(N)で答えを求めることができる。

LL n, k, a[300010], h[300010];
vector<LL>g[300010];
Pll nex[300010], dp[300010];
bool f[300010];
void cmp(Pll &x,Pll y) {
    if (x.fir < y.fir||(x.fir==y.fir&&x.sec>y.sec))
        x = y;
}
void dfs1(LL cur, LL par) {
    nex[cur] = Pll{ -INF / 2,-1 };
    rep(i, g[cur].size()) {
        LL v = g[cur][i];
        if (v == par)continue;
        dfs1(v, cur);
        cmp(nex[cur], { nex[v].fir - 1,nex[v].sec });
        cmp(nex[cur], { a[v] - 1,v });
    }
}
void dfs2(LL cur, LL par) {
    if (par != -1) {
        cmp(dp[cur], { dp[par].fir - 1, dp[par].sec});
        cmp(dp[cur], Pll{ a[par] - 1,par });
    }
    Pll mx = Pll{ -INF / 2,-1 };
    rep(i, g[cur].size()) {
        LL v = g[cur][i];
        if (v == par)continue;
        cmp(dp[v], { mx.fir - 2,mx.sec });
        cmp(mx, nex[v]);
        cmp(mx, { a[v],v });
    }
    reverse(g[cur].begin(), g[cur].end());
    mx = Pll{ -INF / 2,-1 };
    rep(i, g[cur].size()) {
        LL v = g[cur][i];
        if (v == par)continue;
        cmp(dp[v], { mx.fir - 2,mx.sec});
        cmp(mx, nex[v]);
        cmp(mx, { a[v],v });
    }
    rep(i, g[cur].size()) {
        LL v = g[cur][i];
        if (par == v)continue;
        dfs2(v, cur);
    }
    cmp(dp[cur], nex[cur]);
    h[cur] = dp[cur].sec;
}
int main() {
    cin >> n>>k;
    rep(i, n) {
        cin >> a[i];
    }
    rep(i, n - 1) {
        LL x, y;
        cin >> x >> y;
        x--, y--;
        g[x].push_back(y);
        g[y].push_back(x);
    }
    rep(i, n)dp[i] = Pll{ -INF / 2,-1 };
    dfs1(0, - 1);
    dfs2(0, -1);
    LL to = 0;
    while (f[to]==0) {
        f[to] = 1;
        to = h[to];
        k--;
        if (k == 0) {
            cout << to + 1 << endl;
            return 0;
        }
    }
    rep(i, n)f[i] = 0;
    LL cnt = 0;
    while (f[to] == 0) {
        f[to] = 1;
        to = h[to];
        cnt++;
    }
    k %= cnt;
    while (k > 0)to = h[to], k--;
    cout << to + 1 << endl;
    return 0;
}

CS Academy Subarray Medians

リンク : https://csacademy.com/contest/archive/task/subarray-medians/

問題概要

長さNの数列が与えられる。(1<=i<=j<=N かつ j-i=0 mod2)のすべてのi,jに関して[i,j]の中央値をmとしたとき、ijmの総和を求めよ。

解説

最初、iを決めて、前から順にBIT上のa[j]に1を足して、二部探索してO(N2 logn)で求めたら、TLEしてしまった。ここで違うアプローチを考える。すべてのi,jの中央値を考えるのではなく、iがmとして取られるときの総和を求めることにする(割とこの考えは使う)。i<=jのとき、[i,j]に(a[i]より大きい個数)-(a[i]より小さい個数)をdifとする。d[dif]+=jをする。次にj<=iを見た時に、[j,i]に(a[i]より大きい個数)-(a[i]より小さい個数)をdifとすると、d[-dif]ja[i]をとることによってj番目から始まる区間で、中央値がi番目となるときをすべて考えることができる。

LL n, ans, a[11000], d[11000];
int main() {
    cin >> n;
    rep(i, n) {
        cin >> a[i];
    }
    rep(i, n) {
        rep(j, 11000)d[j] = 0;
        LL dif = 5000;
        for (int j = i; j >=0; j--) {
            if (a[i] > a[j])dif++;
            else dif--;
            if (dif <= 10000 && dif >= 0)d[dif] += (j + 1);
        }
        dif = 5000;
        for (int j = i; j < n; j++) {
            if (a[i] >= a[j])dif--;
            else dif++; 
            if (dif <= 10000 && dif >= 0)ans += a[i] * d[dif] * (j + 1);
        }
    }
    cout << ans << endl;
    return 0;
}

CS Academy Maxor

リンク : https://csacademy.com/contest/archive/task/maxor/statement/

問題概要

N個の数字が与えられ、任意の二つの数字をorで計算したときの最大値とこの最大値となる通り数を求めよ。

解説

これもまた高速ゼータ変換...dp1[i]をiを部分集合としてもつ数字の個数とする。これは高速ゼータ変換で簡単に求められる。次に、dp2[i]をiとNこの数字それぞれに&をしたときの最大値とする。この問題の最大値の答えをmとすると、m=max{ {i=1,2,...N}{A[i] or dp2[~A[i]]} }となる。これを求めるほうが、通り数を求めるより難しかった。dp1[i]の数字が正のときはdp2[i]=iとなる。それ以外の場合はdp2[i]=max{ {j=0,1..17}{if(i&(1<<j))dp2[i xor (1<<j)]} }となる。

LL n, ans, dp1[1 << 20], dp2[1 << 20], a[110000];
int main() {
    cin >> n;
    rep(i, n) {
        cin >> a[i];
        dp1[a[i]] += 1;
        dp2[a[i]] = a[i];
    }
    rep(i, 20) rep(b, 1LL << 20)
        if (0 == (b & (1LL << i))) {
            dp1[b] += dp1[b | (1LL << i)];
        }
    rep(b, 1LL << 20)if (dp1[b] > 0)dp2[b] = b;
    Rrep(i, 20) rep(b, 1LL << 20)
        if ((b & (1LL << i))) {
            dp2[b] = max(dp2[b], dp2[b ^ (1LL << i)]);
        }

    LL mx = 0;
    rep(i, n) {
        mx = max(mx, a[i] | dp2[((1LL << 20) - 1)^ a[i] ]);
    }
    rep(i, n) {
        if (mx == a[i]) {
            ans += dp1[0] - 1;
        }
        else if ((mx&a[i]) == a[i]) {
            ans += dp1[mx - a[i]];
        }
    }
    cout <<mx<<" "<< ans/2 << endl;
    return 0;
}

CS Academy Max Score Tree

リンク : https://csacademy.com/contest/archive/task/max-score-tree/statement/

問題概要

N頂点の木とScoreiが与えられる。木のスコアは木に含まれる頂点vが持つ辺の数をEdge[v]とすると、この木のスコアはΣScore[Edge[v]]となる。入力で与えられた木の部分木が満たす最大のスコアを求めよ。

解説

dp[i]を自分の親を部分木として持つ場合の最大のスコアとする。持たない場合も別で計算しておくことも考えておく。頂点vに関して考えるとする。この頂点が持つ辺の集合をEとする。Eのそれぞれに関して、dp[e}(e∈E)をsortして上から貪欲にとって行くことを考える。上からi個足したものをsum[i]とすると、dp[v]=max{sum[i]+Score[i+1]}となる。もし親を持たないときはmax{sum[i]+Score[i]}となる。

LL n, score[110000], dp[110000], ans = -INF;
vector<vector<LL>>g,rvec;
LL func(int cur ,int par) {
    LL ret = score[1], sum = 0;
    if (dp[cur] != -INF)return dp[cur];
    vector<LL>d;
    rep(i, g[cur].size()) {
        LL v = g[cur][i];
        if (v == par)continue;
        func(v, cur);
        d.push_back(dp[v]);
    }
    sort(d.begin(), d.end(), [](LL x, LL y) {return x > y; });
    rep(i, d.size()) {
        sum += d[i];
        ret = max(ret, sum + score[i + 2]);
        ans = max(ans, sum + score[i + 1]);
    }
    return dp[cur] = ret;
}
int main() {
    cin >> n;
    rep(i, n)cin >> score[i];
    g.resize(n);
    rep(i, n - 1) {
        LL x, y;
        cin >> x >> y;
        x--, y--;
        g[x].push_back(y);
        g[y].push_back(x);
    }
    rep(i, n)dp[i] = -INF;
    func(0, -1);
    cout << max(score[0],ans) << endl;
    return 0;
}

CS Academy Distinct Palindromes

リンク : https://csacademy.com/contest/archive/task/distinct-palindromes/statement/

問題概要

文字列が与えられる。この文字列の部分文字列であり、回文であるのは何種類か求めよ。

解説

文字列をSとし、i番目の文字をS[i]とする。dp[l][r]をS[l]=S[r]=σであるときに、必ずlとrを回文に含む[l~r]間の回文の種類数とする。(l~r)の間に文字cが二つ以上存在するとき、(l~r)間にありlから最も近い文字cをc_l番目、rから最も近い文字cをc_r番目とする。σc...cσと取ることになる。dp[l][r]+=dp[c_l][c_r]となる。またこのσのときの間に一つしか文字がないときの場合は、a~zのそれぞれの文字について(l~r)間に存在すればdp[l][r]+=1する。最後にσσのみも考慮してdp[l][r]+=1する。これですべての遷移が終わった。実装する際にa~z以外の文字を番兵にすることで実装が楽になる。 下の図が具体例となる。$を番兵とする。答えに∅を回文として含んでしまうため-1をする。
f:id:akusyounin:20190218224138p:plain
これ典型なんですか?

kmjpさんのコードを参考にしました。

string str;
LL n;
LL dp[1010][1010], r[1010][30], l[1010][30];
LL func(int L, int R) {
    if (dp[L][R] != -1)return dp[L][R];
    LL ret = 1;
    rep(i, 26)if (r[L][i]<R&&l[R][i]>L) {
        ret++;
        if (r[L][i] < l[R][i])ret += func(r[L][i], l[R][i]);
        ret %= MOD;
    }
    return dp[L][R] = ret;
}
int main() {
    cin >> str;
    str = (char)('a' + 26) + str + (char)('a' + 26);
    n = str.size();
    rep(i, n + 1)rep(j, n + 1)dp[i][j] = -1;
    rep(i, n)str[i] -= 'a';
    rep(i, 30)r[n - 1][i] = n, l[0][i] = -1;
    for (int i = n - 2; i >= 0; i--) {
        rep(j, 27) {
            if (str[i + 1] == j)r[i][j] = i + 1;
            else r[i][j] = r[i + 1][j];
        }
    }
    for (int i = 1; i<n; i++) {
        rep(j, 27) {
            if (str[i - 1] == j)l[i][j] = i - 1;
            else l[i][j] = l[i - 1][j];
        }
    }
    cout << (func(0, n - 1)-1+MOD)%MOD << endl;
    return 0;
}