B、Sample Game

题目大意

你有一个随机数生成器,他会随机生成之间某个数,生成的概率为

现在分为大体步操作:

  1. 随机生成一个
  2. 如果这个​是已经生成的数中最大的,返回步骤一继续生成新的数,否则进入步骤三
  3. 游戏结束,本局游戏的得分为生成序列的长度的平方​。

现在要你求出这局游戏的得分期望

Solution

考点:期望dp

这个期望不是很好求,我们优先考虑求出

我们假设​​​是随机到​​​​后还能进行的期望抽取次数,那么我们知道​​,那么我们把之后的抽取分为​​​这三类,可以知道,抽到​​​之后就无法进行了这时候的期望次数就是​​​,如果抽到了相同的数​​​,这时候他带来的期望次数就是​​​,如果抽到了​​​,他们的期望次数是​​​。

我们总结上面三种情况就可以得到下面式子:

进行移相化简,并且知道​之后我们可以得到这样的最终式子。

看出这个是一个倒序的递推式,倒序的处理即可求到的全部值,也就是


接下来题目要求的是的期望,在概率论中,有这样的式子,也就是方差等于平方的期望减掉期望的平方,所以我们不能把划等号。

和求​同理,我们假设​代表随机到​后游戏的期望得分也就是剩余回合数的平方,那么我们知道

​同样的对下次抽取的数字以​分三类情况,可以得到​的递推式。

同样的倒序递推就可以得到全部的值。

下面套用我们求解

套用快速幂和费马小定理求解逆元就出来答案了。

#include <bits/stdc++.h>
using namespace std;
#define int long long
typedef long long ll; typedef unsigned long long ull; typedef long double ld;
inline ll read() { ll s = 0, w = 1; char ch = getchar(); for (; !isdigit(ch); ch = getchar()) if (ch == '-') w = -1; for (; isdigit(ch); ch = getchar())    s = (s << 1) + (s << 3) + (ch ^ 48); return s * w; }
inline void print(ll x, int op = 10) { if (!x) { putchar('0'); if (op)    putchar(op); return; }    char F[40]; ll tmp = x > 0 ? x : -x;    if (x < 0)putchar('-');    int cnt = 0;    while (tmp > 0) { F[cnt++] = tmp % 10 + '0';        tmp /= 10; }    while (cnt > 0)putchar(F[--cnt]);    if (op)    putchar(op); }
ll qpow(ll a, ll b) { ll ans = 1;    while (b) { if (b & 1)    ans *= a;        b >>= 1;        a *= a; }    return ans; }    ll qpow(ll a, ll b, ll mod) { ll ans = 1; while (b) { if (b & 1)(ans *= a) %= mod; b >>= 1; (a *= a) %= mod; }return ans % mod; }
const int MOD = 998244353;

const int N = 100 + 7;
ll n, m;
int w[N], p[N], inv[N];
int f[N], g[N];

inline int add(int a, int b) {
    if (a + b >= MOD)    return a + b - MOD;
    return a + b;
}

int solve() {
    n = read();
    int sum = 0;
    for (int i = 1; i <= n; ++i) {
        w[i] = read();
        sum += w[i];
    }
    int tmp = qpow(sum % MOD, MOD - 2, MOD);
    for (int i = 1; i <= n; ++i) {
        p[i] = w[i] * tmp % MOD;
        inv[i] = qpow((1 - p[i] + MOD) % MOD, MOD - 2, MOD);
    }
    sum = 0;
    for (int i = n; i >= 1; --i) {
        f[i] = add(1, sum) * inv[i] % MOD;
        sum = add(sum, p[i] * f[i] % MOD);
    }
    sum = 0;
    for (int i = n; i >= 1; --i) {
        g[i] = add(add(1, 2 * p[i] * f[i] % MOD), sum) * inv[i] % MOD;
        sum = add(sum, p[i] * add(g[i], 2 * f[i] % MOD) % MOD);
    }
    int res = 0;
    for (int i = 1; i <= n; ++i) {
        res = add(res, add(g[i], (2 * f[i] + 1) % MOD) * p[i] % MOD);
    }
    print(res);

    return 1;
}

signed main() {
    //int T = read(); for (int i = 1; i <= T; ++i)
    {
        solve();
        //cout << (solve() ? "YES" : "NO") << endl;
    }
    return 0;
}

C、LCS

题目大意

你需要构造个长度为的字符串,并且保证

Solution

考点:模拟

我们先不考虑输出顺序,只考虑是否能够构建,我们把变成的形式进行一次排序。

那么我们可以想到下图这样的构建方式是最合理的:

图片说明

接下来就是找出最小的字母,然后确定这三个字符串的位置了。

int solve() {
    string s1, s2, s3;
    int a = read(), b = read(), c = read(), n = read();

    int maxi = max({ a,b,c });
    int mini = min({ a,b,c });
    int tmp = a ^ b ^ c ^ maxi ^ mini;

    if (maxi + tmp - mini > n) {
        return puts("NO"), 0;
    }

    if (mini == b) {
        for (int i = 1; i <= b; ++i) {
            s1 += 'a', s2 += 'a', s3 += 'a';
        }
        for (int i = 1; i <= a - b; ++i) {
            s1 += 'b', s2 += 'b';
        }
        for (int i = 1; i <= c - b; ++i) {
            s1 += 'c', s3 += 'c';
        }
        while (s1.size() < n)    s1 += 'd';
        while (s2.size() < n)    s2 += 'e';
        while (s3.size() < n)    s3 += 'f';
    }
    else if (mini == c) {
        for (int i = 1; i <= c; ++i) {
            s1 += 'a', s2 += 'a', s3 += 'a';
        }
        for (int i = 1; i <= a - c; ++i) {
            s1 += 'b', s2 += 'b';
        }
        for (int i = 1; i <= b - c; ++i) {
            s2 += 'c', s3 += 'c';
        }
        while (s1.size() < n)    s1 += 'd';
        while (s2.size() < n)    s2 += 'e';
        while (s3.size() < n)    s3 += 'f';
    }
    else if (mini == a) {
        for (int i = 1; i <= a; ++i) {
            s1 += 'a', s2 += 'a', s3 += 'a';
        }
        for (int i = 1; i <= b - a; ++i) {
            s2 += 'b', s3 += 'b';
        }
        for (int i = 1; i <= c - a; ++i) {
            s1 += 'c', s3 += 'c';
        }
        while (s1.size() < n)    s1 += 'd';
        while (s2.size() < n)    s2 += 'e';
        while (s3.size() < n)    s3 += 'f';
    }
    cout << s1 << endl;
    cout << s2 << endl;
    cout << s3 << endl;

    return 1;
}

E、Tree Xor

题目大意

你有一个一颗树节点数,树上的边有一个边权,点有一个点权,你要保证有直接父子关系的点之间,并且树上每个点他的点权选取范围为,现在请问你能构造出多少颗不同的树,有任何一个不同就认为是一颗不同的树。

Solution

考点:线段树区间性质

首先树上边权关系给你了,所以当我们顶下后整棵树后续的都会被确定下来,问题就转化成有多少种取值可能。

好,下面我们假设求一遍,这样就可以预处理出每个点的,我们就可以把题目的区间变成下面的形式。

那么全部已知,问题就变成了个区间求并集的问题,并集的大小就是答案了。

但是由于是异或操作,所以本身连续的在异或之后整个区间并不会保持连续,下面我们就要想办法处理出这些分散的区间来。

然后就要想到这个题目的难点了,用线段树的区间性质考虑新建区间,因为线段树最多会分成段区间,并且每段区间都有很明显的前后一致的相关性,那么我们就在线段树上找到这些被包含的区间,然后把他们处理好之后塞进容器里面,最终求解有没有某些部分是个区间相同的并集,这个用一次扫描线即可。

[借用一下这位大佬(lalalzo)的图](2021牛客第四场-E Tree Xor-线段树区间异或_lalalzo的博客-CSDN博客)

图片说明

const int N = 1e5 + 7;
ll n, m;
pai p[N];
vector<pai> G[N];
int a[N];

void dfs(int u, int fa) {
    for (auto& [v, w] : G[u]) {
        if (v == fa)    continue;
        a[v] = a[u] ^ w;
        dfs(v, u);
    }
}

vector<pai> seg;
int bit[40];
#define mid (l + r >> 1)
void build(int l, int r, int cnt, int L, int R, int val) {
    if (L <= l && r <= R) {
        int ql = (l ^ val) & bit[cnt];
        int qr = ql + (1 << cnt) - 1;
        seg.push_back({ ql,qr });
        return;
    }
    if (L <= mid)    build(l, mid, cnt - 1, L, R, val);
    if (R > mid)    build(mid + 1, r, cnt - 1, L, R, val);
}

int solve() {
    bit[0] = (1 << 30) - 1;
    for (int i = 1; i <= 30; ++i) {
        bit[i] = bit[i - 1] ^ (1 << (i - 1));
    }
    n = read();
    for (int i = 1; i <= n; ++i)    p[i] = { read(), read() };
    int u, v, w;
    for (int i = 2; i <= n; ++i) {
        u = read(), v = read(), w = read();
        G[u].push_back({ v,w });
        G[v].push_back({ u,w });
    }
    dfs(1, 0);
    for (int i = 1; i <= n; ++i) {
        build(0, bit[0], 30, p[i].first, p[i].second, a[i]);
    }
    vector<pai> all;
    for (auto& [l, r] : seg) {
        all.push_back({ l,1 });
        all.push_back({ r + 1,-1 });
    }
    sort(all.begin(), all.end());
    int res = 0, tag = 0;
    for (int i = 0; i < all.size(); ++i) {
        tag += all[i].second;
        if (tag == n) {
            res += all[i + 1].first - all[i].first;
        }
    }
    print(res);


    return 1;
}

F、Just a joke

题目大意

两人在进行游戏,每轮游戏操作的玩家可以选择图中的一条边删去,或者找到图中的一棵树删去,最后无法进行操作的玩家就输掉了这局比赛,这张图是有个点条边的无向图。

Solution

我们分别考虑每个操作的势能函数。

删边:是删掉的点数,是删掉的边数,任何时候都为奇数。

删树:,可以看出任何时候也为奇数。

那么这局游戏双方轮流操作,每个人都只能选择或者,说明最终变成的操作次数是确定的,直接判断的奇偶性即可。

int solve() {
    n = read(), m = read();

    return ((n + m) & 1);
}

int main() {
    //int T = read();    rep(_, 1, T)
    {
        //solve();
        cout << (solve() ? "Alice" : "Bob") << endl;
    }
    return 0;
}

G、Product

题目大意

给出,你要构造的序列,对于其中一个序列来说他的贡献是,现在要你求出全部可能的序列贡献之和是多少?

Solution

考点:动态规划+容斥原理+组合数意义

我们首先要推出下面这样的一个式子,可以看出这是个球分成组的可能方案数,那么总的方案数就是,即每个球都可能在每一组。

好,我们回来看题目给出的式子,我们如果把看成一个整体带入我们刚刚上面推出的公式我们就会得到这样的式子。

但是很容易发现一个问题就是题目给出的范围是的但是我们公式的范围是的,这就会导致我们直接计算答案的话会出现很多非法的解,那么我们就要考虑把这些非法的解全部去掉,就要用到容斥原理了。

那么我们知道我们一共有组,那么起始组非法的答案减掉有组非法答案的解加上有组非法答案的解直到容斥系数被算完为止,好那么问题就转换为如何求解有组非法答案的解的数量呢?

这个求解就比较容易了,考虑动态规划代表对于前组当前有个球要分配而言非法答案的方案数。

我们使用三重循环枚举就可以求道这个数组。

接下来答案就会是下面这个式子,从前往后依次是计算答案的修正系数,容斥系数,有多少组非法,多少个球非法并且都要计算组合数,最后是剩余组剩余球合法的方案数。

ll qpow(ll a, ll b) { ll ans = 1;    while (b) { if (b & 1)    ans *= a;        b >>= 1;        a *= a; }    return ans; }    ll qpow(ll a, ll b, ll mod) { ll ans = 1; while (b) { if (b & 1)(ans *= a) %= mod; b >>= 1; (a *= a) %= mod; }return ans % mod; }
const int MOD = 998244353;
const int N = 55 + 7;
ll n, k, D;
int C[N * N][N * N];
int f[N][N * N];

inline int add(int a, int b) {
    if (a + b >= MOD)    return a + b - MOD;
    return a + b;
}

void init() {
    for (int i = 0; i < N * N; ++i) {
        C[i][0] = 1;
        for (int j = 1; j <= i; ++j) {
            C[i][j] = add(C[i - 1][j - 1], C[i - 1][j]);
        }
    }
    f[0][0] = 1;
    for (int i = 0; i < n; ++i)
        for (int j = 0; j <= i * (k - 1); ++j)
            for (int s = 0; s <= k - 1; ++s)
                f[i + 1][j + s] = add(f[i + 1][j + s], f[i][j] * C[j + s][s] % MOD);
}

inline int inv(int x) {
    return qpow(x, MOD - 2, MOD);
}

int solve() {
    n = read(), k = read(), D = read();
    init();

    int res = 0;
    for (int i = 0; i <= n; ++i) {
        int jc = 1;
        int sum = 0;
        int pre = (i & 1) ? MOD - C[n][i] : C[n][i];

        for (int j = 0; j <= i * (k - 1); ++j) {
            int tmp = f[i][j] * qpow(n - i, D + n * k - j, MOD) % MOD * jc % MOD;
            sum = add(sum, tmp);
            jc = jc * ((D + n * k - j) % MOD) % MOD * inv(j + 1) % MOD; // D太大了
        }

        res = add(res, sum * pre % MOD);
    }

    for (int i = D + 1; i <= D + n * k; ++i) {
        res = res * inv(i) % MOD;
    }
    print(res);



    return 1;
}

H、Convolution

题目大意

题目重新定义了运算符:

并且给出长度为,以及常数,定义

我们需要求解的异或和是多少?

Solution

参考题解

我们优先就要转换这个符号的定义,不然这个题目就没办法下手了。

从质因数幂次的关系转换到和他们对应之间的关系就更加容易处理这个关系式了。

那么我们重新写出的定义式

我们考虑枚举

这样由于最后一个求和条件还是涉及除法,我们再把这个除法消掉,我们设

然后可以发现最后面的可以提到前面去,最终把式子写成这样

计算答案的时候我们枚举,对于来说,可能的选择长度是,这个复杂度是,然后根据选择的,又可以确定的更新所以全部的都可以在的时间找到。

还需要注意的一点是,由于原先我们定义的是,现在我们改成了那么在之间就不能有任何其他的公因子了,例如,这样的不能现在计算,因为这样的式子后续一定会枚举到,其实这样这些对应着都是,那么我们就要避免这样的重复计算,我们当且仅当的时候计算一下的值就能保证不多算。

const int MOD = 998244353;
const int N = 1e6 + 7;
ll n, c, a[N];
int p[N], f[N], b[N];

inline int add(int a, int b) {
    if (a + b >= MOD)    return a + b - MOD;
    return a + b;
}

int solve() {
    n = read(), c = read();
    for (int i = 1; i <= n; ++i) {
        a[i] = read();
        p[i] = qpow(i, c, MOD);
    }
    for (int x = 1; x <= n; ++x) {
        for (int g = 1; g * x <= n; ++g) {
            f[g] = add(f[g - 1], p[g] * a[x * g] % MOD);
        }
        for (int y = 1; x * y <= n; ++y) {
            if (gcd(x, y) == 1) {
                b[x * y] = add(b[x * y], p[y] * f[min(n / x, n / y)] % MOD);
            }
        }
    }
    int res = 0;
    for (int i = 1; i <= n; ++i)    res ^= b[i];
    print(res);

    return 1;
}

I、Inverse Pair

题目大意

给你一个初始的排列,你可以选择的任意多位置加上一个,输出最终这个的最小的逆序对数目。

Solution

考点:思维+求逆序对数量

我们考虑哪些位置加了之后他对答案的贡献会减少。

是不是需要一个存在,并且他在的后面,这时候我们把变成就可以减少逆序数。

所以我们从遍历如果我们把某个变成了,那么我们就要打上标记让不能再变成不然就会套娃最终只会让逆序数,然后求逆序数就用树状数组算一次就行了。

const int N = 2e5 + 7;
ll n;
int c[N];

inline int lowbit(int x) { return x & (-x); }
void add(int i, int x) {
    for (; i <= n; i += lowbit(i))    c[i] += x;
}
int query(int i) {
    int res = 0;
    for (; i; i -= lowbit(i)) {
        res += c[i];
    }
    return res;
}

int pos[N], a[N];
bool vis[N];

int solve() {
    n = read();
    for (int i = 1; i <= n; ++i) {
        a[i] = read();
        pos[a[i]] = i;
    }
    for (int i = 2; i <= n; ++i) {
        if (!vis[i - 1] && pos[i - 1] > pos[i]) {
            vis[i] = 1;
            ++a[pos[i - 1]];
        }
    }
    int res = 0;
    for (int i = 1; i <= n; ++i) {
        res += i - 1 - query(a[i]);
        add(a[i], 1);
    }
    print(res);

    return 1;
}

J、Average

题目大意

你有一个的矩阵,这个矩阵的序列给出。

你要再这个矩阵中挑选一个纵向长度大于等于,横向长度大于等于的子矩阵,求子矩阵的最大平均值。

Solution

考点:二分+动态规划

我们假设我们就选择了一个大小为的子矩阵,重新编号之后可以发现这个子矩阵的平均数就等于下面。

那么再裂项之后我们可以知道

那么要化平均数只需要前后都取最大值即可,那么题意就变成了要找到在序列里面选择长度大于的最大平均数。

典中典问题,二分平均数+动态规划判断答案是否存在即可。

稍微提一下动态规划如何判断,我们二分出之后,我们把序列中的数全部减掉就变成了要在序列中找出一个大于等于的和大于等于的子区间。

那么我们维护一个的最小的前缀和,用就可以线性找到区间最大值。

const int N = 1e6 + 7;
ll n, m, x, y;
int a[N], b[N];
double c[N];

const double eps = 1e-8;

bool check(int* a, int n, int L, double mid) {
    c[0] = 0;
    for (int i = 1; i <= n; ++i) {
        c[i] = c[i - 1] + a[i] - mid;
    }
    double ans = -INF64, mini = INF64;
    for (int i = L; i <= n; ++i) {
        mini = min(mini, c[i - L]);
        ans = max(ans, c[i] - mini);
    }
    return ans >= 0;
}

int solve() {
    n = read(), m = read(), x = read(), y = read();
    for (int i = 1; i <= n; ++i)    a[i] = read();
    for (int i = 1; i <= m; ++i)    b[i] = read();

    double l = -1, r = 1e5 + 7, res1 = 0, res2 = 0;

    for (int i = 0; i <= 100; ++i) {
        double mid = (l + r) / 2;
        if (check(a, n, x, mid)) {
            res1 = mid;
            l = mid;
        }
        else
            r = mid;
    }

    l = -1, r = 1e5 + 7;
    for (int i = 0; i <= 100; ++i) {
        double mid = (l + r) / 2;
        if (check(b, m, y, mid)) {
            res2 = mid;
            l = mid;
        }
        else
            r = mid;
    }

    printf("%.10f\n", res1 + res2);

    return 1;
}