牛客多校第十场8月16日补题记录

A Browser Games

题意:给定 n n n 个长度不超过 100 100 100 的字符串,询问第 [ 1 , i ] [1,i] [1,i] 个字符串中最少需要多少个前缀,才能满足这些前缀既能全部包含第 [ 1 , i ] [1,i] [1,i] 个字符串(例如 a \tt a a 包含了 a b c , a d e \tt abc,ade abc,ade,而 b c \tt bc bc 则没有包含 b d e \tt bde bde),并且其后面的字符串不具有这些前缀。需要求出 [ 1 , n ] [1,n] [1,n] 全部的答案。 n ≤ 1 × 1 0 5 n \leq 1\times 10^5 n1×105,字符集等于 62 62 62,并且空间限制 32 MB。

解法:若不考虑空间限制,则是 ICPC 2021 银川站 K 题。其做法为,利用这全部的字符串建立一棵 Trie 树,在建立的过程中,将其出现次数一并插入到 Trie 树种。然后依次遍历这些字符串,每遍历到一个则将其次数对应的减掉,那么本质不同前缀的个数即是互不包含的子树全部 0 0 0 的节点个数。

但是这题空间非常的受限制,因而不可以直接建立完整的 Trie 树,因而需要压缩 Trie 树的空间。正常一颗 Trie 树的空间为 n × l e n n \times len n×len,本题中即为千万级别。而在此题中,我们可以考虑只保留存在多个儿子节点的节点,而删去只有一个儿子的节点。这样的节点数目只有 O ( n ) O(n) O(n) 的(因为只有 n n n 个字符串),是完全可以接受的。同时注意到在我们最初的算法中,也只有存在多个儿子的节点才可能产生贡献,因而这样做是完全可以的。在实现上,先对全部字符串按字典序排序,便可以找到全部的关键节点并建立压缩的 Trie 树。然后用刚刚的方法即可。

还有一个更快、使用空间更少的方法。我们连 Trie 树都不建了,直接考虑每新增一个字符串对答案的影响,维护一个等长的差分数组。考虑当我们插入一个字符串后,某一棵子树上刚好全空了。从前往后看,则是答案在这里会减少整个子树内的前缀贡献;而从后面看,则是从这一字符串往前的一段,前缀数目至少增大 1 1 1

基于这一思想,和第一种方法一样,先对全部字符串进行排序。对于前缀数目的增加,首先考虑第一个字母,按照第一个字母将全部字符串分成若干个子集,那么以第一个字母作为前缀的前缀出现时间应该是在整个这一子集中字符串全部出现完毕后,即子集中最晚字符串出现时间。然后分而治之,每一个子集延续这一操作,考虑按照第二位字符继续划分。对于其减少,一定是在子树中全部长前缀全部出现了,这些长前缀才会消失,并用一个短前缀来代表它们。因而其减少在子集全部出现之时(注意这并不和前面的矛盾,这里指的是长前缀的消失,前文是段前缀的出现),也即子集中出现最晚的字符串出现的时间。

空间复杂度仅存储字符串用的空间与差分数组的空间。

#include <cstdio>
#include <algorithm>
#include <string>
#include <iostream>
using namespace std;
int dif[100005];
struct node
{
    int id;
    string a;
    bool operator <(const node &b)const
    {
        return a < b.a;
    }
};
struct node que[100005];
void dfs(int left,int right,int last,int place)
{
    if(left==right)
        return;
    int nowlast = 0, division = left;
    for (int i = left; i <= right;i++)
    {
        nowlast = max(nowlast, que[i].id);
        if (i == right || que[i].a[place] != que[i + 1].a[place])
        {
            dif[nowlast]++;
            dif[last]--;
            dfs(division, i, nowlast, place + 1);
            division = i + 1;
            nowlast = 0;
        }
    }
    return;
}
int main()
{
    int n;
    scanf("%d", &n);
    for (int i = 1; i <= n;i++)
    {
        que[i].id = i;
        cin >> que[i].a;
    }
    if(n==1)
    {
        printf("1");
        return 0;
    }
    sort(que + 1, que + n + 1);
    dfs(1, n, n + 1, 0);
    int ans = 0;
    for (int i = 1; i <= n;i++)
    {
        ans += dif[i];
        printf("%d\n", ans);
    }
    return 0;
}

C Dance Party

题意: n n n 对男女要配对,但是每一个女生有 k i k_i ki 个不喜欢的男生,问一个能让所有女生都选到没有自己讨厌的男生的配对方案。 n ≤ 3 × 1 0 4 , k i ≤ 100 n \leq 3\times 10^4,k_i\leq 100 n3×104,ki100

解法:显然这是一张稠密图,甚至只有一些边不存在。考虑使用类似根号分治的想法——“分块的本质是对复杂度的均摊”,将男生分为两类,规定一个阈值 d d d,一种是被大多数女生讨厌的(即多于 d d d 个女生讨厌),另一种则是被较少女生讨厌的。显然第一种的人数不会多于 O ( n k d ) O(\displaystyle \frac{nk}{d}) O(dnk),而第二种虽然人数众多但是也很容易匹配成功。

此题的阈值接近 n − k n-k nk,我实现上使用了 n − 200 n-200 n200。对于第一类人,使用常规的匈牙利算法,由于人数不多因而没有压力;对于第二类,由于大部分的匹配关系成立,因而直接匹配即可。

#include <cstdio>
#include <algorithm>
#include <vector>
#include <map>
#include <set>
#include <memory.h>
using namespace std;
const int N = 30000;
bool vis[N + 5], used[N + 5];
int match[N + 5], cnt[N + 5];
map<pair<int, int>, bool> dislike;
vector<int> ava[N + 5];
vector<int> small;//被多数女生讨厌的集合
set<int> big;//大部分男生所处的集合,即不被很多女生讨厌
bool dfs(int u)
{
	for(auto i:ava[u])
		if(vis[i]==0)
		{
            vis[i] = 1;
            if (match[i] == 0 || dfs(match[i]))
            {
                match[i] = u;
                return 1;
			}
		}
	return 0;
}
int main()
{
    int n, k, x;
    scanf("%d", &n);
    for (int i = 1; i <= n;i++)
    {
        scanf("%d", &k);
        for (int j = 1; j <= k;j++)
        {
            scanf("%d", &x);
            cnt[x]++;
            dislike[make_pair(i, x)] = 1;
        }
    }
    for (int i = 1; i <= n;i++)
    {
        if (cnt[i] >= n - 200)
        {
            small.push_back(i);
            for (int j = 1; j <= n;j++)
                if (dislike.count(make_pair(j, i)) == 0)
                    ava[i].push_back(j);
        }
        else
            big.insert(i);
    }
    int tot = 0;
    for(auto i:small)
    {
        memset(vis, 0, sizeof(vis));
        tot += dfs(i);
    }
    if (tot != small.size())
    {
        printf("-1");
        return 0;
    }
    for (int i = 1; i <= n;i++)
    {
        if(match[i])
            continue;
        bool flag = 0;
        for (auto j : big)
            if (dislike.count(make_pair(i, j)) == 0)
            {
                match[i] = j;
                big.erase(j);
                flag = 1;
                break;
            }
        if(!flag)
        {
            printf("-1");
            return 0;
        }
    }
    for (int i = 1; i <= n;i++)
        printf("%d ", match[i]);
    return 0;
}

D Diameter Counting

题意:统计 n n n 个节点全部的无向标号树的直径和。 n ≤ 500 n \leq 500 n500

解法:首先考虑直径如何递推着计算。一种想法是不断删掉周围一圈的叶子,每删去一圈叶子直径减少 2 2 2,直到无法删除为止。若该过程进行了 x x x 轮,则总直径为 2 x 2x 2x 或者 2 x + 1 2x+1 2x+1。考虑其逆过程,即从一个单节点(或者双节点)开始,不断向这棵树的一部分叶子节点添加新的叶子节点(否则直径不增加)。

h i , j h_{i,j} hi,j 为有 j j j 个叶子的 i i i 个节点的树的总直径和。那么答案等于 ∑ i = 1 n h n , i \displaystyle \sum_{i=1}^n h_{n,i} i=1nhn,i。显然有一些是没有的(例如 h n , 1 h_{n,1} hn,1,显然不存在只有一个叶子的树),但是不影响计数。考虑其递推。利用刚刚的“添加一圈叶子直径增大 2 2 2” 的想法,枚举本轮增加的叶子数目 k k k,可以得到每一个 h i , j h_{i,j} hi,j 都会对 h i + k , k h_{i+k,k} hi+k,k 有贡献,其贡献的值为 ( i + k i ) h i , j g i , j , k + 2 ( i + k i ) f i , j g i , j , k \displaystyle \dbinom{i+k}{i}h_{i,j}g_{i,j,k}+2\dbinom{i+k}{i}f_{i,j}g_{i,j,k} (ii+k)hi,jgi,j,k+2(ii+k)fi,jgi,j,k,其中 f i , j f_{i,j} fi,j 表示 i i i 个节点 j j j 个叶子的无向标号树的个数, g i , j , k g_{i,j,k} gi,j,k 表示在 i i i 个节点的树上强制规定要从 j j j 个叶子上连接新的叶子,总共要连接 k k k 个叶子的方案数。其含义为,新的树是由现在的 i + k i+k i+k 个节点中选出 i i i 个节点作为原先的树转移而来,本轮新增的叶子有 g i , j , k g_{i,j,k} gi,j,k 个方案,老树上原本的直径和需要累加;本轮新加的叶子对答案的贡献首先是增大一圈直径增大的 2 2 2,然后一样是选取哪些节点作为老树、老树的总方案数、哪些地方需要接新的叶子。

考虑 f i , j f_{i,j} fi,j 的递推。依旧可以用之前的逻辑——老树接新叶。枚举本轮增加的叶子数 k k k,那么 f i , j f_{i,j} fi,j 就对 f i + k , k f_{i+k,k} fi+k,k 产生贡献,其贡献为 ( i + k i ) f i , j g i , j , k \displaystyle \dbinom{i+k}{i}f_{i,j}g_{i,j,k} (ii+k)fi,jgi,j,k。一样的套路——先是哪些节点构成了原本的树,然后是如何接叶子的。

最后考虑 g i , j , k g_{i,j,k} gi,j,k 的递推。这个递推可以抽象成为长度为 k k k 的序列,每个值可以有 i i i 种取法,但是有 j j j 个值必选,那么 g i , j , k g_{i,j,k} gi,j,k 的含义为前 k k k 个数,每个数的选取范围为 [ 1 , i ] [1,i] [1,i],有 j j j 个值必选的方案数。考虑其递推, g i , j , k = j g i − 1 , j − 1 , k − 1 + i g i , j , k − 1 g_{i,j,k}=jg_{i-1,j-1,k-1}+ig_{i,j,k-1} gi,j,k=jgi1,j1,k1+igi,j,k1。其含义为,如果第 k k k 个数选取了某一个之前未出现的必选的值,那么这个必选值需要枚举,并且之前都不能出现这个数。因而必选范围减少了,所有能选的数的范围也变小了;如果选取了一个之前出现过的必选值或者其他值,那么取值方法有 i i i 种不受任何限制,但是相应的,前面的数还是要满足选了 j j j 个必选的数,但是取值范围没有变还是 [ 1 , i ] [1,i] [1,i]

注意边界条件。三个递推的复杂度均为 O ( n 3 ) O(n^3) O(n3)

#include <cstdio>
#include <algorithm>
using namespace std;
const int N = 500;
long long mod;
long long power(long long a,long long x)
{
    long long ans = 1;
    while(x)
    {
        if(x&1)
            ans = a * ans % mod;
        a = a * a % mod;
        x >>= 1;
    }
    return ans;
}
long long inv(long long a)
{
    return power(a, mod - 2);
}
long long f[N + 5][N + 5], g[N + 5][N + 5][N + 5], h[N + 5][N + 5];
long long fac[2 * N + 5], invfac[2 * N + 5];
long long C(int n,int m)
{
    if(n<0 || m>n)
        return 0;
    else
        return fac[n] * invfac[m] % mod * invfac[n - m] % mod;
}
int main()
{
    int n;
    scanf("%d%lld", &n, &mod);
    fac[0] = fac[1] = 1;
    invfac[0] = invfac[1] = 1;
    for (int i = 2; i <= 2 * N;i++)
        fac[i] = fac[i - 1] * i % mod;
    invfac[2 * N] = inv(fac[2 * N]);
    for (int i = 2 * N - 1; i >= 1; i--)
        invfac[i] = invfac[i + 1] * (i + 1) % mod;
    g[0][0][0] = 1;
    for (int i = 1; i <= n;i++)
    {
        long long cur = 1;
        for (int k = 0; k <= n - i; k++)
        {
            g[i][0][k] = cur;
            cur = cur * i % mod;
        }
    }
    for (int i = 1; i <= n; i++)
        for (int j = 1; j <= i; j++)
            for (int k = j; i + k <= n; k++)
                g[i][j][k] = (j * g[i - 1][j - 1][k - 1] % mod + i * g[i][j][k - 1] % mod) % mod;
    f[1][1] = 1;
    f[2][2] = 1;
    for (int i = 1; i <= n; i++)
        for (int j = 1; j <= i; j++)
            for (int k = j + (i == 1); k + i <= n; k++)
                f[i + k][k] = (f[i + k][k] + f[i][j] * C(i + k, i) % mod * g[i][j][k] % mod) % mod;
    h[2][2] = 1;
    for (int i = 1; i <= n;i++)
        for (int j = 1; j <= i;j++)
            for (int k = j + (i == 1); i + k <= n; k++)
                h[i + k][k] = (h[i + k][k] + h[i][j] * g[i][j][k] % mod * C(i + k, i) % mod + 2 * f[i][j] * C(i + k, k) % mod * g[i][j][k] % mod) % mod;      
    long long ans = 0;
    for (int i = 1; i <= n;i++)
        ans = (ans + h[n][i]) % mod;
    printf("%lld", ans);
    return 0;
}

E More Fantastic Chess Problem

题意:在 k k k 维棋盘上移动以下五种棋子,问在不走出棋盘的情况下一步内可以到多少个格子。

  1. 王: k k k 个维度中任选一些,该维坐标同时增加 1 1 1 或减少 1 1 1
  2. 后: k k k 个维度中任选一些,该维坐标同时增加 x x x 或减少 x x x
  3. 车: k k k 个维度任选一维,该维坐标任意变化。
  4. 相: k k k 个维度任选两不同维,分别增加或减少(可以一个增加一个减少) x x x
  5. 马: k k k 个维度任选两不同维,一维增加 2 2 2 或减少 2 2 2,另一维增加 1 1 1 或减少 1 1 1

并且棋子移动 q q q 次,移动完后都要回答询问。 k , q ≤ 3 × 1 0 5 k,q \leq 3\times 10^5 k,q3×105。棋盘的范围 a i ≤ 1 × 1 0 6 a_i \leq 1\times 10^6 ai1×106

解法:分五个棋子考虑。

  1. 车:最简单的一个。每次的答案都等于 ∑ a i − k \sum a_i-k aik
  2. 王:对维度建立线段树,考虑每个维度上能够怎么移动——增加 1 1 1,不变还是减少。在边界上可能会导致无法增加或减少。最后答案等于这每个维度可能走的结果乘起来减一——扣除完全不动的情况。
  3. 后:对移动距离建立线段树,考虑移动 x x x 格上有多少个维度。考虑第 k k k 个维度对答案的贡献,可以增加 x x x,或者减少 y y y,则移动范围在 [ 0 , min ⁡ ( x , y ) ] [0,\min(x,y)] [0,min(x,y)] 上则是三种——上移、下移或者不变,而在 ( min ⁡ ( x , y ) , max ⁡ ( x , y ) ] (\min(x,y),\max(x,y)] (min(x,y),max(x,y)] 上则是两种——上移(或下移)、不变。最后将答案乘起来减一。
  4. 马:统计全部维度能够上移两步、下移两步、上移一步、下移一步的总和,最后一步总方案乘以两步总方案即可。注意要挖去两个维度选到一起去的情况——挖去上移两步下移一步、上移两步上移一步、下移两步上移一步、下移两步下移一步的总方案数。
  5. 相:和后类似,对移动距离建立线段树,考虑每一个距离下有多少个维度能走,其方案数为 ∑ i = 1 N ( c n t i 2 ) \sum_{i=1}^N \dbinom{cnt_i}{2} i=1N(2cnti),维护其平方和与和即可。

代码很长,注意封装。

F Train Wreck

题意:有一个栈,有一个长度为 2 n 2n 2n 的压入、弹出栈的序列,满足压入次数为 n n n。有 n n n 个待压入栈中的数字序列,问一个合法的序列,使得每次压入栈的栈状态均不同。

解法:提到栈容易想到 dfs 过程。压入即代表了开启新的递归,而弹出则代表了一次回溯。基于这一想法,不难发现这样一个序列代表了一棵树,树上每个点有一个数字,而压入栈状态不同即是表示当前节点到根节点的数字序列都不同。现在需要每一个节点都满足这一条件,那么为了完成这一目标,对于每一个节点,其儿子的数字都应该不同——否则某两个儿子到根节点的数字序列相同了。而对于不同的子树,由于其至少有一个祖先不同,那么数字序列在那一层祖先那里就不一样了,因而不用管。

所以问题转化成为了一个涂色方案使得每一个节点的儿子颜色都不同。自下而上的递归,贪心的将颜色数目最多的 k k k 种颜色分给一个节点的全部 k k k 个儿子即可。

#include <cstdio>
#include <algorithm>
#include <queue>
#include <vector>
using namespace std;
const int N = 1000000;
struct line
{
    int from;
    int to;
    int next;
};
struct line que[N * 2 + 5];
int root, tot;
int cnt, headers[N + 5], father[N + 5];
void add(int from,int to)
{
    cnt++;
    que[cnt].from = from;
    que[cnt].to = to;
    que[cnt].next = headers[from];
    headers[from] = cnt;
}
int color[N + 5];
struct node
{
    int id;
    int times;
    bool operator <(const node &b)const
    {
        return times < b.times;
    }
};
priority_queue<node> q;
bool flag = 1;
void dfs(int place,int father)
{
    if(!flag)
        return;
    int child = 0;
    for (int i = headers[place]; i;i=que[i].next)
        if(que[i].to!=father)
        {
            child++;
            dfs(que[i].to, place);
        }
    if(q.size()<child)
    {
        flag = 0;
        return;
    }
    vector<node> temp;
    for (int i = 1; i <= child;i++)
    {
        temp.push_back(q.top());
        q.pop();
    }
    for (int i = headers[place], j = 0; i; i = que[i].next, j++)
        if(que[i].to!=father)
        {
            color[que[i].to] = temp[j].id;
            temp[j].times--;
            if(temp[j].times)
                q.push(temp[j]);
        }
    return;
}
char a[2 * N + 5];
int times[N * 5];
int main()
{
    int n, x;
    scanf("%d", &n);
    root = n + 1;
    scanf("%s", a + 1);
    int now = root;
    for (int i = 1; i <= 2 * n; i++)
    {
        if(a[i]=='(')//建树过程。左括号表示全新节点,右括号表示回溯
        {
            tot++;
            father[tot] = now;
            add(now, tot);
            add(tot, now);
            now = tot;
        }  
        else
            now = father[now];
    }
    for (int i = 1; i <= n; i++)
    {
        scanf("%d", &x);
        times[x]++;
    }
    for (int i = 1; i <= n;i++)
        if(times[i])
            q.push((node){i, times[i]});
    dfs(root, root);
    if(!flag)
        printf("NO");
    else
    {
        printf("YES\n");
        for (int i = 1; i <= n;i++)
            printf("%d ", color[i]);
    }
    return 0;
}

G Game of Death

题意:有 n n n 个人,每个人朝周围人等概率开枪,每一枪有 p p p 的概率击中,一个人被击中一枪则死亡,问恰有 k k k 人存活的概率。需要求出 ∀ k ∈ [ 0 , n ] \forall k \in [0,n] k[0,n] n ≤ 3 × 1 0 5 n \leq 3\times 10^5 n3×105

解法:首先考虑 k k k 个人一定存活的概率,不要求选定人。那么所有的开枪情况应该是——这 k k k 个人的打这 k k k 个人的枪都没中,而剩下 n − k n-k nk 个人打这 k k k 个人的枪也不能中。考虑反选,这 k k k 人中某一个人打中这 k k k 个人的概率为 ( k − 1 ) p n − 1 \displaystyle \frac{(k-1)p}{n-1} n1(k1)p,而剩下 n − k n-k nk 个人中某一个人击中这 k k k 个人的概率为 k p n − 1 \displaystyle \frac{kp}{n-1} n1kp,因而概率为 ( 1 − ( k − 1 ) p n − 1 ) k ( 1 − k p n − 1 ) n − k \displaystyle (1-\frac{(k-1)p}{n-1})^k(1-\frac{kp}{n-1})^{n-k} (1n1(k1)p)k(1n1kp)nk。记这一概率为 G k G_k Gk

记恰有 k k k 人存活的概率为 F k F_k Fk,考虑用 G k G_k Gk 容斥得到 F k F_k Fk。首先我们需要选定是哪 k k k 个人活下来,然后再依次的枚举剩下人的情况。 F k = ( n k ) ∑ j = k n ( − 1 ) j − k ( n − k j − k ) G j \displaystyle F_k=\dbinom{n}{k}\sum_{j=k}^{n} (-1)^{j-k} \dbinom{n-k}{j-k}G_j Fk=(kn)j=kn(1)jk(jknk)Gj。这一式子的含义为,考虑 k + 1 k+1 k+1 个人活下来的概率(需要减掉),那么一定是在现在已经活下来的 k k k 人中再取一个没有考虑的人,然后减掉。考虑 k + 2 k+2 k+2 个人活下来的概率,它已经被 G k + 1 G_{k+1} Gk+1 减多了几次,所以要补回来,取正号。

将上式中组合数展开,有 F k = n ! k ! ∑ j = k n ( − 1 ) j − k 1 ( j − k ) ! ( n − j ) ! G j \displaystyle F_k=\frac{n!}{k!}\sum_{j=k}^{n} (-1)^{j-k} \frac{1}{(j-k)!(n-j)!} G_j Fk=k!n!j=kn(1)jk(jk)!(nj)!1Gj,这就转化成为了一个多项式卷积的形式,直接使用 FFT 即可通过。

  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值