CH3401:石头游戏题解

毒瘤思路

对,这道题是毒瘤题。首先,我们可以判定出,这些序列一定会在\(60s\)内全部从头开始,因为\(1,2,3,4,5,6\)的最大公倍数为\(60\)。之后,我们可以考虑把这些操作放入操作矩阵之中。操作矩阵的长宽都为\(n*m\)。具体的操作对应如下:

  • 在本题中,我们把一个二维的点映射为一个数,公式为\(num(i,j)=(i-1)m+j\)。
  • 对于把格子值设置为数字的操作:
    1. 首先把操作矩阵\(A[0][num(i,j)]\)设置为操作数。
    2. 之后,把矩阵\(A[num(i,j)][num(i,j)]\)设置为\(1\),这是为了使其在矩阵乘法中得到保留。
  • 而对于移动操作:
    • 如果是\(E\),那么把\(A[num(i,j)][num(i,j+1)]\)设置为一,以此类推

然后算出\(p=\lfloor \frac{m}{60} \rfloor, q = m \mod 60\),进行矩阵快速幂和连续乘法即可。

好毒瘤,建议不要写。

代码

// CH3401.cpp
#include <bits/stdc++.h>
#define ll long long
using namespace std;
const int MAX_N = 70;
ll N, M, t, act;
char opt[MAX_N][MAX_N];
string opts[MAX_N];
struct matrix
{
    ll mat[MAX_N][MAX_N], n, m;
    matrix() { memset(mat, 0, sizeof(mat)); }
    matrix(int n, int m)
    {
        this->n = n, this->m = m;
        for (int i = 1; i <= max(n, m); i++)
            mat[i][i] = 1;
    }
    matrix operator*(const matrix &tar) const
    {
        matrix ans = matrix();
        ans.n = n, ans.m = tar.m;
        for (int i = 0; i <= n; i++)
            for (int j = 0; j <= tar.m; j++)
                for (int k = 0; k <= m; k++)
                    ans.mat[i][j] += mat[i][k] * tar.mat[k][j];
        return ans;
    }
    matrix operator^(const int &tim) const
    {
        int ti = tim;
        matrix ans, bas = *this;
        for (int i = 0; i <= max(n, m); i++)
            ans.mat[i][i] = 1;
        ans.n = n, ans.m = m;
        while (ti)
        {
            if (ti & 1)
                ans = ans * bas;
            bas = bas * bas;
            ti >>= 1;
        }
        return ans;
    }
} P, Q, mats[70];
ll getPos(ll x, ll y) { return (x - 1) * M + y; }
int main()
{
    scanf("%lld%lld%lld%lld", &N, &M, &t, &act);
    for (int i = 1; i <= N; i++)
        for (int j = 1; j <= M; j++)
            cin >> opt[i][j], opt[i][j] -= '0';
    for (int i = 0; i < act; i++)
        cin >> opts[i];
    int p = t / 60, q = t % 60;
    matrix F;
    F.n = 0, F.m = getPos(N, M), F.mat[0][0] = 1;
    for (int tim = 1; tim <= 60; tim++)
    {
        matrix current;
        current.n = getPos(N, M), current.m = getPos(N, M);
        current.mat[0][0] = 1;
        for (int i = 1; i <= N; i++)
            for (int j = 1; j <= M; j++)
            {
                int pos = (tim - 1) % (opts[opt[i][j]].size());
                char op = opts[opt[i][j]][pos];
                switch (op)
                {
                case 'N':
                    if (i > 1)
                        current.mat[getPos(i, j)][getPos(i - 1, j)] = 1;
                    break;
                case 'S':
                    if (i < N)
                        current.mat[getPos(i, j)][getPos(i + 1, j)] = 1;
                    break;
                case 'E':
                    if (j < M)
                        current.mat[getPos(i, j)][getPos(i, j + 1)] = 1;
                    break;
                case 'W':
                    if (j > 1)
                        current.mat[getPos(i, j)][getPos(i, j - 1)] = 1;
                    break;
                case 'D':
                    break;
                default:
                    current.mat[0][getPos(i, j)] = op - '0';
                    current.mat[getPos(i, j)][getPos(i, j)] = 1;
                    break;
                }
            }
        mats[tim] = current;
    }
    P = mats[1];
    for (int i = 2; i <= 60; i++)
        P = P * mats[i];
    P = P ^ p;
    Q = q == 0 ? matrix(getPos(N, M), getPos(N, M)) : mats[1];
    for (int i = 2; i <= q; i++)
        Q = Q * mats[i];
    F = F * P * Q;
    ll ans = 0;
    for (int i = 1; i <= getPos(N, M); i++)
        ans = max(ans, F.mat[0][i]);
    printf("%lld", ans);
    return 0;
}

CH3602:Counting Swaps 题解

主要思路

这是道好题,运用了图论建模的方法,可谓是十分精妙了。

对于每一个\(i\)与\(arr[i]\)都连一条有向边,比如说数列\(2\ 1\ 4\ 3\):

可以通过 DFS 染色的方式找出\(k\)个环,并且将每个环的长度记为\(l_i\)。我们可以得出一个引理:

在图中,对于长度为\(n\)的环,将其变为自环的最小步骤为\(n-1\)。

就是这样的性质,我们可以记\(F[i]\)为当环长为\(i\)时变为自环的方案数,那么我们可以基于分裂的思想,设\(T(x,y)\)为当环长为\(x+y\)时分裂为环长分别为\(x,y\)时的方案数,显然:

\[T(x,y) = \begin{cases} \frac{n}{2}, x = y \\ n, x \neq y \end{cases}\]

所以,我们可以照例推出:

\[ F[i] = \sum_{x+y=i} T(x,y)*F[x]*F[y]*\frac{(n-2)!}{(x-1)!(y-1)!} \]

我来解释一下这个式子的意义:首先,枚举\(x\)和\(y\)的情况,然后乘上这两个本身变成自环的方案数,也就是\(F[x]*F[y]\),之后根据多重集排列公式和上面那个引理,两者步数分别为\(x-1,y-1\),所以也不难得出上面那一坨了。

答案为\(\prod_{i = 1}^k {F_{l_i}} * \frac{(n-k)!}{\prod_{i = 1}^{k}(l_i-1)}\),时间复杂度为\(O(n^2)\),然而通过人类智慧等方法,发现\(F[n]=n^{n-2}\),所以通过快速幂降为\(O(n \log n)\)。

代码

// CH3602.cpp
#include <bits/stdc++.h>
#define ll long long
using namespace std;
const int MAX_N = 1e5 + 200, mod = 1e9 + 9;
int T, n, arr[MAX_N], head[MAX_N], current, k, li[MAX_N], vis[MAX_N];
ll level[MAX_N], inv[MAX_N];
struct edge
{
    int to, nxt;
} edges[MAX_N << 1];
void addpath(int src, int dst)
{
    edges[current].to = dst, edges[current].nxt = head[src];
    head[src] = current++;
}
int dfs(int u, int fa)
{
    vis[u] = true;
    for (int i = head[u]; i != -1; i = edges[i].nxt)
        if (edges[i].to != fa)
        {
            if (vis[edges[i].to])
                return 1;
            return dfs(edges[i].to, u) + 1;
        }
    return 1;
}
ll quick_power(ll bas, ll tim)
{
    ll ans = 1;
    while (tim)
    {
        if (tim & 1)
            ans = ans * bas % mod;
        bas = bas * bas % mod;
        tim >>= 1;
    }
    return ans;
}
ll Fn(int n) { return (n == 1) ? 1 : quick_power(n, n - 2); }
int main()
{
    scanf("%d", &T);
    level[0] = inv[0] = 1;
    for (int i = 1; i < MAX_N; i++)
        level[i] = level[i - 1] * i % mod;
    inv[MAX_N - 1] = quick_power(level[MAX_N - 1], mod - 2);
    for (int i = MAX_N - 1; i >= 2; i--)
        inv[i - 1] = inv[i] * i % mod;
    while (T--)
    {
        memset(head, -1, sizeof(head)), current = 0;
        memset(li, 0, sizeof(li)), k = 0;
        memset(vis, 0, sizeof(vis));
        scanf("%d", &n);
        for (int i = 1; i <= n; i++)
            scanf("%d", &arr[i]), addpath(arr[i], i), addpath(i, arr[i]);
        for (int i = 1; i <= n; i++)
            if (!vis[i])
                li[++k] = dfs(i, 0);
        ll ans = 1;
        for (int i = 1; i <= k; i++)
            ans = ans * Fn(li[i]) % mod;
        ans = (ans * level[n - k] % mod);
        for (int i = 1; i <= k; i++)
            ans = ans * inv[li[i] - 1] % mod;
        printf("%lld\n", ans);
        continue;
    }
    return 0;
}

POJ1737:Connected Graph 题解

主要思路

这道题的计数方程和思想非常有趣,下面开始进行分析。

我们设\(F[i]\)为点数为\(i\)时的无向连通图种类。我们可以尝试容斥:\(n\)个点的无向图种类有\(2^{\frac{n(n-1)}{2}}\),我们可以枚举\(k\),删去大小为\(k\)的连通块来保持连通性。因为本题要求点标号,所以这些连通块的种类为:

\[\sum_{k=1}^{i-1}(F[k]*C_{i-1}^{j-1}*2^{\frac{(i-k)(i-k-1)}{2}})\]

其中,\(F[k]\)为连通块大小为\(k\)时的连通块个数,然后\(C_{i-1}^{j-1}\)是标号对答案的贡献,之后另一个残余块的种类为\(2^{\frac{(i-k)(i-k-1)}{2}}\),乘起来即可。

这道题的加强版需要使用 NTT 和卷积的知识:BZOJ 3456:城市规划题解

代码

// POJ1737.cpp
//#include <bits/stdc++.h>
#include <iostream>
#include <cstdio>
#define ll long long
using namespace std;
const int MAX_N = 60;
ll f[MAX_N], C[MAX_N][MAX_N], tmp;
int main()
{
    C[1][0] = C[1][1] = 1;
    for (int i = 2; i < MAX_N; i++)
    {
        C[i][0] = 1;
        for (int j = 1; j < MAX_N; j++)
            C[i][j] = C[i - 1][j] + C[i - 1][j - 1];
    }
    f[1] = 1;
    for (int i = 2; i < MAX_N; i++)
    {
        f[i] = (1 << ((i * (i - 1)) >> 1));
        for (int j = 1; j < i; j++)
            f[i] -= f[j] * C[i - 1][j - 1] * (1 << (((i - j) * (i - j - 1)) >> 1));
    }
    while (~scanf("%d", &tmp) && tmp != 0)
        printf("%lld\n", f[tmp]);
    return 0;
}

「Codeforces 559C」Gerald and Giant Chess 题解

主要思路

这一道题其实挺好的,我们来分析一下。

首先,我们知道在一个方格纸上,从左上角到右下角的路径的数量是\(C_{H+W-2}^{H-1}\),可以理解为横向路径的选法总数。之后,我们来进行容斥。在本题中,我们需要容斥掉那些经过了至少一次黑点的路径数量。因为本题中黑点个数小于\(5000\),所以我们直接来跑一个\(O(n^2)\)的 DP。设\(F[i]\)为经过了此点的路径数,然后我们把右下角的点设为第\(n+1\)个点,最后答案存在\(F[n+1]\)处。我们可以很快得到一个递推式:

\[ F[i] = C_{x_i + y_i – 2}^{x_i – 1} – \sum_{j = 1}^{i-1} (F[j] * C_{x_i-x_j+y_i-y_j}^{x_i-x_j}),且x_i \geq x_j, y_i \geq y_j. \]

解释:对于到点\(i\)的路径,首先不考虑之前的黑点则有\( C_{x_i + y_i – 2}^{x_i – 1} \)种路;而要容斥掉至少经过过一次的黑点的路径,可以直接对于之前的每一个点进行计算,计算之前的点\(j\)到现在的点\(i\)有多少种路径。

代码

// CF559C.cpp
#include <bits/stdc++.h>
#define ll long long
#define pr pair<ll, ll>
using namespace std;
const int MOD = 1e9 + 7, MAX_N = 2020, LEVEL_RG = 200000;
ll level[200010], inv[200010], n, h, w, F[MAX_N];
pr pts[MAX_N];
ll quick_power(ll bas, ll tim, ll mod)
{
    ll ans = 1;
    while (tim)
    {
        if (tim & 1)
            ans = ans * bas % mod;
        bas = bas * bas % mod;
        tim >>= 1;
    }
    return ans;
}
ll C(ll n, ll m) { return level[n] * inv[n - m] % MOD * inv[m] % MOD; }
int main()
{
    scanf("%d%d%d", &h, &w, &n);
    for (int i = 1; i <= n; i++)
        scanf("%d%d", &pts[i].first, &pts[i].second);
    sort(pts + 1, pts + 1 + n);
    pts[n + 1].first = h, pts[n + 1].second = w;
    level[0] = inv[0] = 1;
    for (int i = 1; i <= LEVEL_RG; i++)
        level[i] = level[i - 1] * i % MOD;
    ll inv_bas = quick_power(level[LEVEL_RG], MOD - 2, MOD);
    for (int i = LEVEL_RG; i >= 1; i--)
        inv[i] = inv_bas, inv_bas = inv_bas * i % MOD;
    for (int i = 1; i <= n + 1; i++)
    {
        F[i] = C(pts[i].first + pts[i].second - 2, pts[i].first - 1) % MOD;
        for (int j = 1; j < i; j++)
            if (pts[j].first <= pts[i].first && pts[j].second <= pts[i].second)
                F[i] = (F[i] - F[j] * C(pts[i].first + pts[i].second - pts[j].first - pts[j].second, pts[i].first - pts[j].first)) % MOD;
    }
    printf("%lld", (F[n + 1] + MOD) % MOD);
    return 0;
}

Educational DP Contest : I – Coins 题解

主要思路

概率 DP,显然地。

我们怎么来统计呢?在存下概率之后,我们枚举轮数并且枚举硬币向上的情况,之后转移即可,代码里一目了然。

代码

// I.cpp
#include <bits/stdc++.h>
using namespace std;
const int MAX_N = 3030;
int n;
double prob[MAX_N], dp[MAX_N][MAX_N];
int main()
{
    scanf("%d", &n);
    for (int i = 1; i <= n; i++)
        scanf("%lf", &prob[i]);
    dp[0][0] = 1;
    for (int i = 1; i <= n; i++)
        for (int j = 0; j <= i; j++)
        {
            int x = j, y = i - j;
            if (x)
                dp[x][y] += dp[x - 1][y] * prob[i];
            if (y)
                dp[x][y] += dp[x][y - 1] * (1 - prob[i]);
        }
    double ans = 0;
    for (int i = 0; n - i > i; i++)
        ans += dp[n - i][i];
    printf("%.10lf", ans);
    return 0;
}

Educational DP Contest : J – Sushi 题解

主要思路

啊呀,我太菜了。

因为每盘的寿司数量最多为3,所以我们可以三个参数分别代表寿司数量为\(i\)的盘数。然后我们可以得出这样的方程:

\[ tot = A + B + C, dp(A,B,C) = \frac{n}{tot} + \frac{A}{tot}*dp(A-1,B,C) + \frac{B}{tot}*dp(A+1,B-1,C) + \frac{C}{tot}*dp(A,B+1,C-1) \]

因为没有固定的顺序进行 DP,所以我们进行记忆化搜索即可。

代码

// J.cpp
#include <bits/stdc++.h>
using namespace std;
const int MAX_N = 330;
int n, ai[MAX_N], sushi[4];
double dp[MAX_N][MAX_N][MAX_N];
double dfs(int a, int b, int c)
{
    if (a == 0 && b == 0 && c == 0)
        return 0;
    if (dp[a][b][c] >= 0)
        return dp[a][b][c];
    double d = a + b + c, ans = n / d;
    if (a)
        ans += dfs(a - 1, b, c) * a / d;
    if (b)
        ans += dfs(a + 1, b - 1, c) * b / d;
    if (c)
        ans += dfs(a, b + 1, c - 1) * c / d;
    return dp[a][b][c] = ans;
}
int main()
{
    memset(dp, -1, sizeof(dp));
    scanf("%d", &n);
    for (int i = 1; i <= n; i++)
        scanf("%d", &ai[i]), sushi[ai[i]]++;
    printf("%.10lf", dfs(sushi[1], sushi[2], sushi[3]));
    return 0;
}