2023 年牛客多校第七场题解

A Random Addition

题意:给定长度为 n n n 的数列,初始全为 0 0 0。对其中 m m m 个区间 [ l i , r i ] [l_i,r_i] [li,ri] 执行加 x x x 操作, x x x 等概率从 [ 0 , 1 ] [0,1] [0,1] 实数集合选取。这些区间包含或不相交。 q q q 次询问整个序列最大值在 [ p , q ] [p,q] [p,q] 的概率对 998   244   353 998\ 244\ 353 998 244 353 取模。 1 ≤ n ≤ 1 0 5 1 \le n \le 10^5 1n105 1 ≤ m ≤ 200 1 \le m \le 200 1m200 0 ≤ p ≤ q ≤ 10 0 \le p \le q \le 10 0pq10

解法:首先考虑一个数字仅被加一次,则该数字取值的概率密度函数为:
P ( x ) = f 1 ( x ) = { 1 , 0 ≤ x ≤ 1 0 , o t h e r w i s e P(x)=f_1(x)= \begin{cases} 1,0 \le x \le 1\\ 0,{\rm otherwise} \end{cases} P(x)=f1(x)={1,0x10,otherwise
如果加两次,则该数字的取值概率密度函数就需要使用到卷积:
f 2 ( x ) = P ( x ) ∗ P ( x ) = ∫ − ∞ + ∞ P ( t ) P ( x − t ) d t = { x , 0 ≤ x ≤ 1 2 − x , 1 < x ≤ 2 0 , o t h e r w i s e \begin{aligned} f_2(x)&=P(x)*P(x)\\ &=\int_{-\infty}^{+\infty}P(t)P(x-t){\rm d}t\\ &=\begin{cases} x,0 \le x \le 1\\ 2-x,1 f2(x)=P(x)P(x)=+P(t)P(xt)dt= x,0x12x,1<x20,otherwise
即,两个窗函数的卷积是一个三角波函数。

如果该数字加 k k k 次,则可以考虑将 P ( x ) P(x) P(x) 进行 k k k 次卷积。使用数学归纳法证明该 f k ( x ) f_k(x) fk(x) 一定是一个 k k k 段函数,每段函数都可以使用一个多项式表达。假设经过第 k − 1 k-1 k1 次卷积后整个函数分为 k − 1 k-1 k1 段,其中第 j j j j ∈ [ 1 , k − 1 ] j \in [1,k-1] j[1,k1])个分段函数对应区间为 [ j − 1 , j ] [j-1,j] [j1,j]。考虑对这个函数再做一次卷积:
f k , j ( x ) = ∫ − ∞ + ∞ f k − 1 , j ( t ) P ( x − t ) d t = ∫ x − 1 x f k − 1 , j ( t ) d t = { ∫ j − 1 x f k − 1 , j ( t ) d t , x ∈ [ j − 1 , j ] ∫ x − 1 j f k − 1 , j ( t ) d t , x ∈ [ j , j + 1 ] \begin{aligned} f_{k,j}(x)&=\int_{-\infty}^{+\infty}f_{k-1,j}(t)P(x-t){\rm d}t\\ &=\int_{x-1}^{x}f_{k-1,j}(t){\rm d}t\\ &= \begin{cases} \displaystyle \int_{j-1}^{x}f_{k-1,j}(t){\rm d}t, x\in[j-1,j]\\ \displaystyle \int_{x-1}^{j}f_{k-1,j}(t){\rm d}t, x\in[j,j+1]\\ \end{cases} \end{aligned} fk,j(x)=+fk1,j(t)P(xt)dt=x1xfk1,j(t)dt= j1xfk1,j(t)dt,x[j1,j]x1jfk1,j(t)dt,x[j,j+1]
若记 F ( x ) = ∫ 0 x f ( t ) d t \displaystyle F(x)=\int_{0}^x f(t){\rm d}t F(x)=0xf(t)dt,则卷积式可写为:
f k , j ( x ) = { F ( x ) − F ( j − 1 ) , x ∈ [ j − 1 , j ] F ( j ) − F ( x − 1 ) , x ∈ [ j , j + 1 ] f_{k,j}(x)= \begin{cases} \displaystyle F(x)-F(j-1), x\in[j-1,j]\\ \displaystyle F(j)-F(x-1), x\in[j,j+1]\\ \end{cases} fk,j(x)={F(x)F(j1),x[j1,j]F(j)F(x1),x[j,j+1]
回到本题,由于需要求的是小于等于某个特定数字的概率,因而需要对概率密度函数进行积分得到概率分布函数,不妨直接维护概率分布函数。

由于题目中有个强约束——区间包含或者不相交,因而这些区间组成一个树形结构,可以考虑 dfs 遍历得到每个区间上的概率分布函数。当子树中存在多个不相交的直接子区间时,父节点需要将这些区间的概率密度函数一一通过多项式卷积乘到对应分段区间上,然后父区间再自行进行卷积运算。这么做的原因是因为对于两个独立变量的最大值的概率分布函数 Pr ⁡ ( max ⁡ ( X 1 , X 2 ) ≤ x ) = Pr ⁡ ( X 1 ≤ x ) Pr ⁡ ( X 2 ≤ x ) \Pr(\max(X_1,X_2) \le x)= \Pr(X_1 \le x)\Pr(X_2 \le x) Pr(max(X1,X2)x)=Pr(X1x)Pr(X2x)

最后查询仅需要查询根节点的区间(即整个数组)上的概率分布函数即可。如果使用朴素多项式乘法(卷积),总复杂度 O ( 10 m 3 ) \mathcal O(10m^3) O(10m3)

关于多项式函数的卷积和乘积,可以参看附录乙·乘积、卷积部分。

感谢致远星的代码:

#include 
using namespace std;
const int mod = 998244353;
int n, m, t;
struct qq
{
    int l, r;
    bool operator<(const qq &b)const
    {
        if (r != b.r)
            return r < b.r;
        return l > b.l;
    }
} a[210];
int sta[210], top, inv[1001000];
vector<int> g[210];
struct P
{
    int xs[210], len;
    // 积分
    inline void integral()
    {
        for (int i = len; i >= 0; i--)
            xs[i + 1] = 1ll * xs[i] * inv[i + 1] % mod;
        xs[0] = 0;
        len++;
    }
    // 单点求值(点值)
    inline int eval(int x)
    {
        int ss = 0;
        for (int j = 0, w = 1; j <= len; j++, w = 1ll * w * x % mod)
            (ss += 1ll * w * xs[j] % mod) %= mod;
        return ss;
    }
} dp[210][410], E, C, fz[210];
// dp[i][j]:第i个区间的第j个分段函数区间[j,j+1]的多项式函数表达式
// 注意:dp[i][j]是概率分布函数,即概率密度函数的积分
// 两个多项式函数卷积
P mul(P a, P b)
{
    P c;
    c.len = a.len + b.len;
    for (int i = 0; i <= c.len; i++)
        c.xs[i] = 0;
    for (int i = 0; i <= a.len; i++)
        for (int j = 0; j <= b.len; j++)
            (c.xs[i + j] += 1ll * a.xs[i] * b.xs[j] % mod) %= mod;
    return c;
}
int c[410][410];
// 反褶,f(x)->f(1-x)
// 虽然f(x)仅在[0,inf]上定义,但是也可以进行定义域的补充,使得它是一个奇函数
P reverseAndShift(P a)
{
    P b;
    b.len = a.len;
    for (int i = 0; i <= b.len; i++)
        b.xs[i] = 0;
    for (int i = 0; i <= a.len; i++)
        (b.xs[0] += a.xs[i]) %= mod, (b.xs[i] -= a.xs[i]) %= mod;
    return b;
}
// 叠加两个多项式函数
void add(P &a, P b)
{
    if (a.len < b.len)
        swap(a, b);
    for (int i = 0; i <= b.len; i++)
        (a.xs[i] += b.xs[i]) %= mod;
}
int d[410];
void dfs(int x)
{
    if (g[x].empty())
    {
        dp[x][0] = E, dp[x][1] = C, d[x] = 1;
        return;
    }
    dp[x][0] = C, d[x] = 0;
    for (int v : g[x])
    {
        dfs(v);
        // 各个子树的概率分布函数要和父节点进行卷积以进行合并
        // 即,当前这一段的概率分布函数为f1(x),现在需要乘到父节点概率分布函数的这一段上
        for (int z = 0; z <= max(d[x], d[v]); z++)
            fz[z] = mul(dp[x][min(z, d[x])], dp[v][min(z, d[v])]);
        d[x] = max(d[x], d[v]);
        for (int z = 0; z <= d[x]; z++)
            dp[x][z] = fz[z];
    }
    // 考虑对这个区间整体做一次卷积运算
    if (x)
    {
        // 最右侧的区间平移
        dp[x][d[x] + 1] = dp[x][d[x]], d[x]++;
        // 其他区间由于对1卷积,因而等价于直接积分
        for (int i = 0; i <= d[x]; i++)
            dp[x][i].integral();
        // 前一区间跟后一区间的叠加
        for (int i = d[x]; i; i--)
            add(dp[x][i], reverseAndShift(dp[x][i - 1]));
        // 补足x->inf的时候概率分布函数=1的部分,增加新的一段分段
        dp[x][d[x] + 1] = C, d[x]++;
    }
}
const int T = 1000000;
int S(int x)
{
    int e = x % T;
    x /= T; // 查在哪个区间(分段函数)之中
    e = 1ll * e * inv[T] % mod;
    return dp[0][min(x, d[0])].eval(e);
}
int main()
{
    E.len = 1, E.xs[1] = 1;
    C.len = 0, C.xs[0] = 1;
    inv[1] = 1;
    for (int i = 2; i <= T; i++)
        inv[i] = mod - 1ll * inv[mod % i] * (mod / i) % mod;
    c[0][0] = 1;
    for (int i = 1; i <= 400; i++)
    {
        c[i][0] = 1;
        for (int j = 1; j <= i; j++)
            c[i][j] = (c[i - 1][j - 1] + c[i - 1][j]) % mod;
    }
    scanf("%d%d%d", &n, &m, &t);
    for (int i = 1; i <= m; i++)
        scanf("%d%d", &a[i].l, &a[i].r);
    sort(a + 1, a + m + 1);
    for (int i = 1; i <= m; i++)
    {
        while (top && a[sta[top]].l >= a[i].l)
            g[i].push_back(sta[top]), top--;
        sta[++top] = i;
    }
    for (int i = 1; i <= top; i++)
        g[0].push_back(sta[i]);
    dfs(0);
    for (int i = 1, l, r; i <= t; i++)
    {
        scanf("%d%d", &l, &r);
        printf("%d\n", ((S(r) - S(l)) % mod + mod) % mod);
    }
    return 0;
}

C Beautiful Sequence

题意:给定长度为 n n n 的序列 { a } i = 1 n \{a\}_{i=1}^n {a}i=1n 的异或差分序列 { b } i = 1 n − 1 \{b\}_{i=1}^{n-1} {b}i=1n1,要求 { a } \{a\} {a} 不严格递增且数字范围都在 [ 0 , 2 30 − 1 ] [0,2^{30}-1] [0,2301] 的第 k k k 大的序列。 1 ≤ n ≤ 1 0 6 1 \le n \le 10^6 1n106 1 ≤ k < 2 30 1 \le k < 2^{30} 1k<230

解法:不难注意到给定了异或差分数组后,确定了第一个数字就确定了整个数组。由于是异或操作,考虑分位处理。

从高到低的枚举第 k k k 个二进制位。这时考虑如何通过异或差分数组得到递增条件。如果 b i = 0 b_i=0 bi=0,则 a i = a i + 1 a_i=a_{i+1} ai=ai+1,符合条件;如果 b i = 1 b_i=1 bi=1,则必须有 0 = a i < a i + 1 = 1 0=a_i0=ai<ai+1=1,要么就是更高的二进制位上已经能够明确分出大小。因而可以考虑维护一个 vector 表示这一段仍未分出大小,需要通过更低位去判断,而块间不需要判断。因而块内必须有 0 = a i < a i + 1 = 1 0=a_i0=ai<ai+1=1。如果当前这个二进制位没有约束,则可以 0 , 1 0,1 0,1。否则就必须强制填 0 , 1 0,1 0,1。这样的第 k k k 大只需要把可以变化的位拿出来,将 k k k 进行二进制表示即可。复杂度 O ( n log ⁡ V ) \mathcal O(n \log V) O(nlogV)

#include 
#define fp(i, a, b) for (int i = a, i##_ = b; i <= i##_; ++i)
#define fd(i, a, b) for (int i = a, i##_ = b; i >= i##_; --i)

using namespace std;
using ll = long long;
const int N = 1e6 + 5;
int n, k, a[N], w[30];
void Solve() {
    scanf("%d%d", &n, &k), --k;
    fp(i, 2, n) scanf("%d", a + i), a[i] ^= a[i - 1];
    vector<int> vec;
    vector<pair<int, int>> s = {{1, n}}, t;
    memset(w, -1, sizeof w);
    fd(j, 29, 0) {
        for (auto [l, r] : s) {
            int fg = 0;
            fp(i, l + 1, r) {
                if ((a[i] >> j & 1) != (a[l] >> j & 1)) {
                    fp(k, i + 1, r)
                        if ((a[k] >> j & 1) != (a[i] >> j & 1))
                            return puts("-1"), void();
                    int x = a[l] >> j & 1;
                    if (w[j] != -1 && w[j] != x)
                        return puts("-1"), void();
                    w[j] = x, fg = 1;
                    t.push_back({l, i - 1}), t.push_back({i, r});
                    break;
                }
            }
            if (!fg) t.push_back({l, r});
        }
        s = t, t.clear();
    }
    int x = 0;
    fp(i, 0, 29) {
        if (w[i] == -1)
            x |= (k & 1) << i, k >>= 1;
        else x |= w[i] << i;
    }
    if (k) puts("-1");
    else {
        fp(i, 1, n) printf("%d%c", x ^ a[i], " \n"[i == n]);
    }
}
int main() {
    int t = 1;
    scanf("%d", &t);
    while (t--) Solve();
    return 0;
}

E Star Wars

题意:在一个无向图 G ( n , m ) G(n,m) G(n,m) 上,可能有重边。一次操作可以执行下面的其中一条:

  1. ( u , v ) (u,v) (u,v) 上连接一条边。
  2. 去除 ( u , v ) (u,v) (u,v) 连接的一条边。
  3. 修改 a u a_u au 权值。
  4. 查询 u u u 相邻节点 a v a_v av 的权值和。

操作次数 1 ≤ q ≤ 3 × 1 0 5 1 \le q \le 3\times 10^5 1q3×105 1 ≤ n , m ≤ 3 × 1 0 5 1 \le n,m \le 3\times 10^5 1n,m3×105

解法:对于这类维护周围点连接情况的题,很容易想到根号分治,对大点(度数大的点)和小点(度数小的点)进行分开考虑。

对于大点小点的划分,通常来说使用根号为界限——当度数小于 O ( n ) O\left(\sqrt n\right) O(n ) 时,可以暴力去搜周围的点,单次查询复杂度 O ( n ) O\left(\sqrt n\right) O(n );对于度数较大的点,由于边数有限(假设 n , m n,m n,m 同阶),这样大点数目本身较少,只有 O ( n ) O\left(\sqrt n\right) O(n ) 个。因而可以考虑对大点进行一些特殊的数据结构处理以快速维护。

对于本题,当维护点权值更新时,小点可以暴力外推到周围所有的点,更新它们的答案,让大点的答案被动被小点所更新;大点对大点也可以考虑暴力,因为大点本身不多;大点对小点则采取标记的方式,在大点处打上标记,让小点来被动查询大点的更新情况。

当边数发生变化时,需要动态调整大小点的划分,当一个小点连接大量的边后需要晋升为大点以加速操作。同时根据上述规则同步更新每个点到周围点的答案。

感谢HDU-T04的提交。

#include 
#define pb push_back
#define fir first
#define sec second
#define ll long long
using namespace std;
typedef pair<int, int> pii;
const int N = 3e5 + 10, K = sqrt(N);
// 块阈值为K
const int P = (1 << 30);
int n, q, tot, d[N], cnt[N];
ll w[N], res[N];
vector<pii> e[N], G[N];
map<int, int> mp[N];
int TOT;
map<int, int> idx;
void add(int x, int y, int id)
{
    if (d[x] >= K)
        G[y].pb({x, id});
    else
        e[x].pb({y, id});
}
// 由小点晋升为大点
void upd(int x)
{
    for (auto E : e[x])
    {
        G[E.fir].pb({x, E.sec});
        if (cnt[E.sec])
            res[x] += w[E.fir];
    }
}
int main()
{
    ios::sync_with_stdio(false);
    cin.tie(0), cout.tie(0);
    cin >> n >> q;
    int lst = 0;
    while (q--)
    {
        int opt, x, y;
        cin >> opt >> x, x ^= lst;
        if (!idx.count(x))
            idx[x] = ++TOT, x = TOT;
        else
            x = idx[x];
        if (opt == 4)
        {
            // 大点等于被动更新值+本地值
            if (d[x] >= K)
            {
                cout << res[x] + w[x] << endl;
                lst = (res[x] + w[x]) % P;
            }
            else // 小点暴力搜周围的点
            {
                ll ans = w[x];
                for (auto E : e[x])
                    if (cnt[E.sec])
                        ans += w[E.fir];
                cout << ans << endl;
                lst = ans % P;
            }
        }
        else
        {
            cin >> y, y ^= lst;
            if (opt == 1)
            {
                // 先维护边
                if (!idx.count(y))
                    idx[y] = ++TOT, y = TOT;
                else
                    y = idx[y];
                if (x == y)
                    continue;
                if (y < x)
                    swap(x, y);
                int id;
                if (!mp[x].count(y))
                {
                    mp[x][y] = ++tot, id = tot;
                    add(x, y, id), add(y, x, id);
                }
                else
                    id = mp[x][y];
                cnt[id]++;
                if (cnt[id] == 1)
                {
                    // x为大点:被动更新
                    if (d[x] >= K)
                        res[x] += w[y];
                    if (d[y] >= K)
                        res[y] += w[x];
                }
                d[x]++, d[y]++;
                if (d[x] == K)
                    upd(x);
                if (d[y] == K)
                    upd(y);
            }
            else if (opt == 2)
            {
                if (!idx.count(y))
                    idx[y] = ++TOT, y = TOT;
                else
                    y = idx[y];

                if (x == y)
                    continue;
                if (y < x)
                    swap(x, y);
                int id = mp[x][y];
                cnt[id]--;
                if (!cnt[id])
                {
                    // 回退
                    if (d[x] >= K)
                        res[x] -= w[y];
                    if (d[y] >= K)
                        res[y] -= w[x];
                }
            }
            else
            {
                w[x] += y;
                for (auto E : G[x])
                    if (cnt[E.sec])
                        res[E.fir] += y;
            }
        }
    }
    return 0;
}

F Counting Sequences

题意:问有多少个长度为 n n n 的序列 { A } = { a 1 , a 2 , ⋯   , a n } \{A\}=\{a_1,a_2,\cdots,a_n\} {A}={a1,a2,,an} 满足,它与自身循环右移一位的序列 { B } = { a 2 , a 3 , ⋯   , a n , a 1 } \{B\}=\{a_2,a_3,\cdots,a_n,a_1\} {B}={a2,a3,,an,a1} 异或得到的序列 { C } \{C\} {C} 中,每个数字二进制表示中 1 1 1 的数目有 k k k 个,且 0 ≤ a i < 2 m 0 \le a_i <2^m 0ai<2m 1 ≤ n < 998   244   353 1 \le n <998\ 244\ 353 1n<998 244 353 1 ≤ m ≤ 1 0 8 1 \le m \le 10^8 1m108 1 ≤ k ≤ 5 × 1 0 4 1 \le k \le 5\times 10^4 1k5×104

解法:由于是异或操作,因而每位独立。考虑对于一个特定的二进制位,原序列 { A } \{A\} {A} 与右移一位的 { B } \{B\} {B} 异或后得到的异或结果序列 { C } \{C\} {C} 性质,有:

  1. 一个异或结果序列对应于原来的两个序列。不难注意到 c i = a i + 1 ⊕ a i c_i=a_{i+1}\oplus a_i ci=ai+1ai,因而 { C } \{C\} {C} 等价于 { A } \{A\} {A} 的异或差分序列。如果设定 a 1 a_1 a1,则整个序列都可以通过逐步递推得到,并且 a 1 a_1 a1 任意。因而 a 1 a_1 a1 的两种取值对应于原序列的两种取值。
  2. 如果异或结果序列的 1 1 1 个数为奇数,则原序列无解。因为 c i = a i ⊕ a i + 1 c_i=a_i \oplus a_{i+1} ci=aiai+1(此处 a n + 1 = a 1 a_{n+1}=a_1 an+1=a1),则 ⨁ i = 1 n c i = ⨁ i = 1 n a i a i + 1 = 0 \bigoplus_{i=1}^n c_i=\bigoplus_{i=1}^n a_ia_{i+1}=0 i=1nci=i=1naiai+1=0,因为每个数字都出现了两次。因而 c i c_i ci 1 1 1 个数必然为偶数。

因而对于一个二进制位,异或结果序列在这一位上产生 k k k 1 1 1 的方案可以用一个多项式 f f f 表达:
f ( x ) = ∑ i = 0 ⌊ n 2 ⌋ 2 ( n 2 i ) x 2 i f(x)=\sum_{i=0}^{\left \lfloor \frac{n}{2}\right \rfloor} 2\binom{n}{2i}x^{2i} f(x)=i=02n2(2in)x2i
即,奇数个 1 1 1 的方案不存在,偶数个 1 1 1 的方案等价于在 n n n 个数中选出 k k k 个数字的方案乘以 2 2 2

由于此处需要计算 m m m 个二进制位上得到 1 1 1 个数总和为 k k k 的方案,由于各行独立,因而使用乘法原理将每一行的多项式乘起来,不难得到答案为 [ x k ] f m ( x ) [x^k]f^m(x) [xk]fm(x)。由于求的目标项系数仅为 k k k 次,因而进行快速幂的多项式 f f f 也不需要保留到 n n n 次,而仅需要 k k k 次即可,更高次项显然不会对答案有任何贡献。使用多项式快速幂(多项式 exp 和多项式 ln 配合)即可在 O ( k log ⁡ m ) \mathcal O(k \log m) O(klogm) 的复杂度内求解。

关于生成函数的一些基础知识,请参看附录·甲 生成函数入门部分。

int main()
{
    long long n, m, k;
    cin >> n >> m >> k;
    Poly f(k + 1);
    long long C = 1;
    for (int i = 0; i <= k; i++)
    {
        if (i % 2 == 0)
            f[i] = C;
        C = C * (n - i) % P;
        C = C * inv[i + 1] % P;
    }
    Poly ans = f.pow(m, k + 1);
    long long cur = ans[k];
    printf("%d", cur * fpow(2, m) % P);
    return 0;
}

G Cyperation

题意:给定长度为 n n n 的环 { a } i = 1 n \{a\}_{i=1}^n {a}i=1n,每次可以选择环上最近距离为 k k k 的两点 i , j i,j i,j 使得 a i a_i ai a j a_j aj 都减 1 1 1。问能不能经过若干次操作让整个环上数字清零。多测, 1 ≤ T ≤ 1 0 6 1 \le T \le 10^6 1T106 2 ≤ ∑ n ≤ 1 0 6 2 \le \sum n \le 10^6 2n106 1 ≤ k ≤ n 1 \le k \le n 1kn

解法:首先特判全 0 0 0 k > ⌊ n 2 ⌋ k>\left \lfloor\dfrac{n}{2} \right \rfloor k>2n 的情况。首先可以考虑将能够一起操作的数字通过重新排列放置到一起,由于每个数字只能至多和两个数字一起减 1 1 1,因而必定可以成环。整个大环就可以分成若干个独立的小环单独处理。

这时考虑环 a 0 , a 1 , ⋯   , a k a_0,a_1,\cdots,a_k a0,a1,,ak,设 a 0 a_0 a0 a 1 a_1 a1 一起减了 x x x 次。则 a 1 a_1 a1 再想要减到 0 0 0 a 1 a_1 a1 就必须和 a 2 a_2 a2 一起减 a 1 − x a_1-x a1x 次,同理可以推得 a 2 a_2 a2 a 3 a_3 a3 一起减 a 2 − a 1 + x a_2-a_1+x a2a1+x 次,依次类推。

不难注意到每传递一轮 x x x 的系数正负号翻转一次。可以把环上这每个次数的一次函数表达式写出。显然这些次数都是非负数,因而维护一个合法的 x x x 区间 [ l , r ] [l,r] [l,r] 即可。同时,当环长为偶数时,经过一圈的递推可以得到 a 0 a_0 a0 a 1 a_1 a1 要一起减 x + b x+b x+b 次。如果 b ≠ 0 b \ne 0 b=0 则无解;如果环长为奇数,则一圈递推后 a 0 a_0 a0 a 1 a_1 a1 的次数为 b − x b-x bx。这时需要满足 2 x = b 2x=b 2x=b,可以解出固定的整数 x x x b b b 为奇数则直接无解)。带入检查这个 x x x 是否合法即可。

#include 
#define fp(i, a, b) for (int i = a, i##_ = int(b); i <= i##_; ++i)
#define fd(i, a, b) for (int i = a, i##_ = int(b); i >= i##_; --i)

using namespace std;
using ll = long long;
const int N = 2e6 + 5;
ll a[N];
int n, k, vis[N];
void Clear() { fp(i, 0, n - 1) vis[i] = 0; }
struct node {
    ll a, b;
    node operator-(const node &x) const
    {
        return {a - x.a, b - x.b};
    }
    node operator+(const node &x) const
    {
        return {a + x.a, b + x.b};
    }
};
bool Checker(vector<long long> &num) {
    int m = num.size();
    vector<node> cur(m);
    cur[0] = (node){1ll, 0ll};
    ll l = 0, r = num[0];
    fp(i, 0, m - 1)
        cur[(i + 1) % m] = (node){0ll, num[(i + 1) % m]} - cur[i];
    fp(i, 0, m - 1)
        if (cur[i].a == -1) // -x+b>=0 x<=b
            r = min(r, cur[i].b);
        else if (cur[i].a == 1) // x+b>=0 x>=-b
            l = max(l, -cur[i].b);
        else assert(false);
    if (m % 2) {
        if (cur[0].b % 2)
            return false;
        ll x = cur[0].b / 2;
        // printf("CUR X:%lld\n", x);
        if (x < l || x > r)
            return false;
    }
    else {
        if (cur.back().b != num[0])
            return false;
    }
    return l <= r;
}
void Solve() {
    scanf("%d%d", &n, &k);
    fp(i, 0, n - 1) scanf("%lld", &a[i]);
    bool zero = 1;
    fp(i, 0, n - 1) if (a[i]) zero = 0;
    if (zero) return (void)puts("YES");
    if (k > n / 2)
        return (void)puts("NO");
    bool fg = true;
    fp(i, 0, n - 1)
        if (!vis[i]) {
            vector<long long> cur;
            int pos = i;
            while (!vis[pos]) {
                cur.push_back(a[pos]);
                vis[pos] = 1;
                pos = (pos + k) % n;
            }
            fg &= Checker(cur);
        }
    if (fg) puts("YES");
    else puts("NO");
}
int main() {
    int t = 1;
    scanf("%d", &t);
    while (t--) Solve(), Clear();
    return 0;
}

I We Love Strings

题意:给定 n n n 个仅含 0,1,? 的正则串,? 可以匹配一个 01,问有多少个 01 串可以被至少一个正则串匹配。 1 ≤ n ≤ 400 1 \le n \le 400 1n400 ∑ ∣ s i ∣ ≤ 400 \sum |s_i| \le 400 si400

解法:由于 ∑ ∣ s i ∣ ≤ 400 \sum |s_i| \le 400 si400,不难想到根号分治——首先根据串长对串进行分类。对于串长小于等于 20 20 20 的,可以考虑直接枚举这 2 k 2^k 2k 个串,依次观察是否和这些正则串匹配;当串长长的时候,正则串个数不多。这时可以用 f S f_{S} fS 表示至少满足 S S S 集合内的正则串的串个数是多少。暴力计算 f S f_S fS——枚举有哪些正则串,然后逐位合并。然后使用类似与卷积的容斥递推得到恰好仅满足该集合内的串个数是多少。最后统计对非空集合求和即可。复杂度 O ( n 2 n ) \mathcal O\left(n2^{\sqrt n}\right) O(n2n )

#include 
using namespace std;
const int N = 400, LIM = 20, P = 998244353;
string s[N + 5];
long long th[N + 5];
class Solution
{
    vector<string> p;
    long long bigSolver(int len, int size)
    {
        vector<long long> f(1 << size);
        f[0] = th[len];
        string base = "";
        for (int j = 0; j < len; j++)
            base += "?";
        for (int i = 1; i < 1 << size; i++)
        {
            string cur = base;
            bool flag = 1;
            for (int j = 0; j < size && flag; j++)
                if (i >> j & 1)
                {
                    for (int k = 0; k < len; k++)
                        if (p[j][k] != '?')
                        {
                            if (cur[k] == '?')
                                cur[k] = p[j][k];
                            else if (cur[k] != p[j][k])
                            {
                                flag = 0;
                                break;
                            }
                        }
                }
            if (flag)
                f[i] = th[count(cur.begin(), cur.end(), '?')];
        }
        for (int i = 0; i < 1 << size; i++)
            for (int j = 0; j < size; j++)
                if (i >> j & 1)
                    f[i ^ (1 << j)] = (f[i ^ (1 << j)] - f[i] + P) % P;
        long long ans = 0;
        for (int i = 1; i < 1 << size; i++)
            ans = (ans + f[i]) % P;
        return ans;
    }
    long long smallSolver(int len)
    {
        long long ans = 0;
        for (int i = 0; i < 1 << len; i++)
        {
            bool flag = 0;
            for (auto x : p)
            {
                bool cur = 1;
                for (int j = 0; j < len; j++)
                {
                    if (x[j] == '?')
                        continue;
                    if (x[j] != (i >> j & 1) + '0')
                    {
                        cur = 0;
                        break;
                    }
                }
                if (cur)
                {
                    flag = 1;
                    break;
                }
            }
            if (flag)
                ans++;
        }
        return ans;
    }

public:
    void insert(string s)
    {
        p.push_back(s);
    }
    long long query()
    {
        if (p.empty())
            return 0;
        int len = p[0].length();
        if (len <= LIM)
            return smallSolver(len);
        else
            return bigSolver(len, p.size());
    }
} S[N + 5];
int main()
{
    th[0] = 1;
    for (int i = 1; i <= N; i++)
        th[i] = th[i - 1] * 2 % P;
    int n;
    scanf("%d", &n);
    for (int i = 1; i <= n; i++)
    {
        cin >> s[i];
        S[s[i].length()].insert(s[i]);
    }
    long long ans = 0;
    for (int i = 1; i <= N; i++)
        ans = (ans + S[i].query()) % P;
    printf("%lld", ans);
    return 0;
}

K Set

题意:给定长度为 n n n 的序列 { a } i = 1 n \{a\}_{i=1}^n {a}i=1n,求下式:
∑ S ⊆ { a } i = 1 n ∣ S ∣ ( min ⁡ x ∈ S x ) ( max ⁡ y ∈ S y ) ( ⨁ z ∈ S z ) \sum_{S \subseteq \{a\}_{i=1}^n} |S|\left(\min_{x \in S}x\right)\left(\max_{y \in S}y\right)\left (\bigoplus_{z \in S}z\right) S{a}i=1nS(xSminx)(ySmaxy)(zSz)
1 ≤ n ≤ 1 0 6 1 \le n \le 10^6 1n106 0 ≤ a i < 2 30 0 \le a_i < 2^{30} 0ai<230

解法:首先对序列排序,那么考虑固定区间左右端点就可以确定 min ⁡ \min min max ⁡ \max max。由于出现异或,仍然考虑在这一位上拆位计算,即:
∑ k = 0 29 2 k ∑ S ∈ { a } i = 1 n ∣ S ∣ ( min ⁡ x ∈ S x ) ( max ⁡ y ∈ S y ) [ [ 2 k ] ( ⨁ z ∈ S z ) = 1 ] \sum_{k=0}^{29} 2^k \sum_{S \in \{a\}_{i=1}^n} |S|\left(\min_{x \in S}x\right)\left(\max_{y \in S}y\right)\left[[2^k]\left(\bigoplus_{z \in S}z\right)=1\right] k=0292kS{a}i=1nS(xSminx)(ySmaxy)[[2k](zSz)=1]
考虑枚举右端点,观察所有区间异或和为 1 1 1 的左端点和集合大小的乘积。考虑用 f f f 数组维护所有合法方案中当前异或和是 0 0 0 还是 1 1 1 min ⁡ x \min x minx 的和, g g g 数组表示所有合法方案的 ∣ S ∣ min ⁡ x |S|\min x Sminx 的和,进行递推。

每当加入一个数字 k k k,假设当前它在这一位上二进制位是 y y y,那么对于 f f f(截至目前枚举到的数字中,所有合法选择状态下 min ⁡ x \min x minx 的和)的更新,有三种情况:

  1. 仅选择当前数字。则 f y ← k f_y \leftarrow k fyk
  2. 不选择当前数字。则 f 0 ← f 0 f_0 \leftarrow f_0 f0f0 f 1 ← f 1 f_1 \leftarrow f_1 f1f1
  3. 之前所有的方案加入当前的数字,则 min ⁡ x \min x minx 本身不变,但是所有方案下 min ⁡ x \min x minx 的和发生变化。则新的 f 0 ← f x f_0 \leftarrow f_x f0fx f 1 ← f x ⊕ 1 f_1 \leftarrow f_{x \oplus 1} f1fx1

对于 g g g,同样分为这三类:

  1. 仅选择当前数字。则 g y ← k × 1 g_y \leftarrow k\times 1 gyk×1
  2. 不选择当前数字。则 g 0 ← g 0 g_0 \leftarrow g_0 g0g0 g 1 ← g 1 g_1 \leftarrow g_1 g1g1
  3. 之前所有的方案加入当前的数字,则 ∣ S ∣ min ⁡ x |S|\min x Sminx 本身要变成 ( ∣ S ∣ + 1 ) min ⁡ x (|S|+1)\min x (S+1)minx。则新的 g 0 ← f x + g x g_0 \leftarrow f_x+g_x g0fx+gx g 1 ← f x ⊕ 1 + g x ⊕ 1 g_1 \leftarrow f_{x \oplus 1}+g_{x \oplus 1} g1fx1+gx1

按照上式递推更新即可。总复杂度 O ( n log ⁡ V ) \mathcal O(n \log V) O(nlogV)

#include 
#define fp(i, a, b) for (int i = a, i##_ = b; i <= i##_; ++i)
#define fd(i, a, b) for (int i = a, i##_ = b; i >= i##_; --i)

using namespace std;
using ll = long long;
const int N = 1e6 + 5, P = 998244353;
int n, a[N];
void Solve() {
    scanf("%d", &n);
    fp(i, 1, n) scanf("%d", a + i);
    sort(a + 1, a + n + 1);
    int ans = 0;
    fp(j, 0, 30) {
        vector<ll> f = {0, 0}, g = f, w;
        fp(i, 1, n) {
            int x = a[i] >> j & 1, sum;
            w = {0, 0}, w[x] = a[i], sum = (w[1] + f[x ^ 1] + g[x ^ 1]) % P;
            ans = (ans + (1ll << j) * a[i] % P * sum) % P;
            g = {(g[0] + w[0] + f[x] + g[x]) % P, (g[1] + sum) % P};
            f = {(f[0] + w[0] + f[x]) % P, (f[1] + w[1] + f[x ^ 1]) % P};
        }
    }
    printf("%d\n", ans);
}
int main() {
    int t = 1;
    // scanf("%d", &t);
    while (t--) Solve();
    return 0;
}

L Misaka Mikoto’s Dynamic KMP Problem

题意:给定串 S S S,一次操作可以执行下面的其中一条:

  1. 修改 S S S 中一个字符。
  2. 给定串 T T T,问 S S S T T T 中出现多少次,以及 S S S 的 border 长度。输出它们的乘积。

操作次数 1 ≤ q ≤ 1 0 6 1 \le q \le 10^6 1q106 ∑ ∣ T ∣ , S ≤ 2 × 1 0 6 \sum |T|,S \le 2\times 10^6 T,S2×106。强制在线。

解法:这题最大需要注意到的点就是输出的是匹配次数乘以 border 长度。因而当 ∣ t ∣ < ∣ s ∣ |t|<|s| t<s 时,border 长度是无需计算的因为匹配次数必然为 0 0 0;而当 t t t 长的时候,暴力匹配 s s s t t t,由于限制了 ∑ ∣ t ∣ \sum |t| t,因而这样暴力执行计算的次数不会很多,至多每次 ∣ t ∣ = ∣ s ∣ |t|=|s| t=s。这样总复杂度仅为 O ( 2 ∑ ∣ T ∣ ) \mathcal O\left(2\sum |T|\right) O(2T)

#include 
using namespace std;
class KMP
{
    vector<int> nx;
    vector<long long> b;

public:
	KMP(vector<long long> &b)
	{
        this->b = b;
        int n = b.size();
		int j = 0;
		nx.resize(n);
		for (int i = 1; i < n; i++)
		{
			while (j > 0 && b[i] != b[j])
				j = nx[j - 1];
			if (b[i] == b[j])
				j++;
			nx[i] = j;
    	}
	}
	int find(vector<long long> &a)
	{
		int n = b.size(), m = a.size();
		int j = 0;
		long long ans = 0;
		for (int i = 0; i < m; i++)
		{
			while (j > 0 && a[i] != b[j])
				j = nx[j - 1];
			if (a[i] == b[j])
				j++;
			if (j == n)
			{
				//匹配位点:i-n+1
				ans++;
				j = nx[j - 1];
			}
    	}
    	return ans;
	}
    int getBorder()
    {
        return nx.back();
    }
};
int main()
{
    long long b, mod, x, pos;
    int n, q, op;
    scanf("%d%d%lld%lld", &n, &q, &b, &mod);
    vector<long long> s(n);
    for (int i = 0; i < n; i++)
        scanf("%lld", &s[i]);
    long long cur = 1, ans = 0, lastans = 0;
    while (q--)
    {
        scanf("%d", &op);
        if (op == 1)
        {
            scanf("%lld%lld", &pos, &x);
            s[(pos ^ lastans) % n] = x ^ lastans;
        }
        else
        {
            cur = cur * b % mod;
            scanf("%lld", &pos);
            vector<long long> t(pos);
            for (auto &x : t)
            {
                scanf("%lld", &x);
                x ^= lastans;
            }
            if (t.size() < s.size())
            {
                lastans = 0;
                continue;
            }
            KMP solve(s);
            lastans = 1ll * solve.find(t) * solve.getBorder();
            ans = (ans + lastans % mod * cur % mod) % mod;
        }
    }
    printf("%lld", ans);
    return 0;
}

M Writing Books

题意: q q q 次询问 [ 1 , n ] [1,n] [1,n] 中数位个数的和。 1 ≤ q ≤ 1 0 5 1 \le q \le 10^5 1q105 1 ≤ n ≤ 1 0 9 1 \le n \le 10^9 1n109

解法:枚举 [ 1 0 k , 1 0 k + 1 − 1 ] [10^k,10^{k+1}-1] [10k,10k+11],这里面每个数字数位个数都是 k k k。枚举 k ∈ [ 1 , 9 ] k \in [1,9] k[1,9] 即可。复杂度 O ( q log ⁡ V ) \mathcal O(q \log V) O(qlogV)

#include 
#define fp(i, a, b) for (int i = a, i##_ = b; i <= i##_; ++i)
#define fd(i, a, b) for (int i = a, i##_ = b; i >= i##_; --i)

using namespace std;
using ll = long long;
const int N = 2e5 + 5;
int n; ll f[10];
void Solve() {
    ll x, k = 0, i = 10;
    scanf("%lld", &x);
    for (; i <= x; i *= 10, ++k);
    // printf("%d %d %lld\n", k, x - i / 10 + 1, f[k - 1] + (x - i / 10 + 1) * (k + 1));
    printf("%lld\n", f[k - 1] + (x - i / 10 + 1) * (k + 1));
}
int main() {
    f[0] = 9;
    for (ll i = 1, k = 90; i <= 9; ++i, k *= 10)
        f[i] = f[i - 1] + (ll)k * (i + 1);
    // fp(i, 0, 9) printf("%lld ", f[i]);puts("");
    int t = 1;
    scanf("%d", &t);
    while (t--) Solve();
    return 0;
}

附录

在阅读下列背景知识前,最好有一些 信号与系统 的背景知识。

甲 生成函数入门

在研究离散时间信号的时候,会提到一种变换:Z 变换。对于一个单边离散时间信号 f ( t ) f(t) f(t),会定义它的 Z 变换为 Z ( z ) = ∑ i = 0 + ∞ f ( i ) z i \mathscr Z(z)=\displaystyle \sum_{i=0}^{+\infty} f(i)z^i Z(z)=i=0+f(i)zi,用一个函数来维护一个序列的性质。

考虑用一个函数来维护序列。首先考虑一个最基本的问题:

一个盒子中有 n n n 个完全相同的球,从中拿出 k k k 个球的方案数是多少?

这个问题的答案是显然的: ( n k ) \displaystyle \binom{n}{k} (kn)。当 k = 0 k=0 k=0 时答案是 ( n 0 ) \displaystyle \binom{n}{0} (0n) k = 1 k=1 k=1 时答案为 ( n 1 ) \displaystyle \binom{n}{1} (1n),依次类推。考虑把 k = 0 , 1 , ⋯   , n − 1 , n , n + 1 , ⋯ k=0,1,\cdots,n-1,n,n+1,\cdots k=0,1,,n1,n,n+1, 的答案记为一个数列 { f } \{f\} {f},可以得到:
( n 0 ) , ( n 1 ) , ⋯   , ( n k ) , ⋯   , ( n n − 1 ) , ( n n ) , 0 , 0 , ⋯ \binom{n}{0},\binom{n}{1},\cdots,\binom{n}{k},\cdots,\binom{n}{n-1},\binom{n}{n},0,0,\cdots (0n),(1n),,(kn),,(n1n),(nn),0,0,
如果我们不希望每次都书写这么长的序列来记录这个问题的答案,可以考虑引入一个新的符号 x x x——它不可以和一般的数字进行加减乘除计算,当且仅当 x x x 的指数相同的时候才可以进行系数的加减运算。和 Z 变换相同,我们进行同样的变化:
( n 0 ) x 0 + ( n 1 ) x 1 + ⋯ + ( n k ) x k + ⋯ + ( n n − 1 ) x n − 1 + ( n n ) x n + 0 x n + 1 + 0 x n + 2 ⋯ \binom{n}{0}x^0+\binom{n}{1}x^1+\cdots+\binom{n}{k}x^k+\cdots+\binom{n}{n-1}x^{n-1}+\binom{n}{n}x^n+0x^{n+1}+0x^{n+2}\cdots (0n)x0+(1n)x1++(kn)xk++(n1n)xn1+(nn)xn+0xn+1+0xn+2
这时我们会发现这个求和式可以使用二项式定理变成 ( x + 1 ) n (x+1)^n (x+1)n。因而我们这时就可以使用一个简单的函数 ( x + 1 ) n (x+1)^n (x+1)n 来去记录这个问题的一般答案。如果我们想知道这个序列的第 k k k 项,我们就可以去查询它的第 k k k 次项的系数,这个系数就表达了这个问题的答案。

但是引入这个记号还不足以带来足够的方便,我们希望这个式子它本身具有一定的组合意义。回到原问题: n n n 个盒子里面拿出 k k k 个球的方案,这时我们观察我们的 ( x + 1 ) n (x+1)^n (x+1)n,问题可以等价变换于——给定 n n n 个括号,每个括号里面可以选择 1 1 1 或者 x x x,问从这 n n n 个括号中选择 k k k x x x 的方案是什么。这两个问题显然完全等价,因而不难发现选 k k k x x x 的方案等价于 ( x + 1 ) n (x+1)^n (x+1)n x k x^k xk 项系数。我们尝试将 ( x + 1 ) n (x+1)^n (x+1)n 和我们的原问题进行映射。首先原问题中每个球取或者不取是相互独立的,且每个球只有取或者不取两种方案。而 ( x + 1 ) n (x+1)^n (x+1)n 中的乘法 ( x + 1 ) ⋅ ( x + 1 ) ⋯ ( x + 1 ) (x+1)\cdot(x+1)\cdots(x+1) (x+1)(x+1)(x+1) 本质对应于各个球取或者不取的独立性, x + 1 x+1 x+1 对应于当前球取或者不取的两种方案,只能二择其一。即多项式上定义的乘法运算,对应了乘法原理;而加法运算对应了加法原理。至此,任何排列组合问题都可以使用多项式上的这两种运算进行定义和表示。

通常来说,一般的计数问题得到的多项式函数是平凡的,不能用一些很简单的标记方法合并。但是使用 FFT、NTT 等 O ( n log ⁡ n ) \mathcal O(n \log n) O(nlogn) 的时间复杂度优秀的多项式卷积算法就可以快速计算一些卷积递推式,以达到快速计算的目的。

乙 乘积、卷积

在信号上,严格定义的多项式(离散时间信号)乘积和卷积运算如下:

  1. 乘积:
    f ( x ) ⋅ g ( x ) = f ( x ) g ( x ) = ∑ i = − ∞ + ∞ f i g i x i f(x)\cdot g(x)=f(x)g(x)=\sum_{i=-\infty}^{+\infty}f_ig_ix^i f(x)g(x)=f(x)g(x)=i=+figixi

  2. 卷积:
    f ∗ g ( x ) = ∫ − ∞ + ∞ f ( τ ) g ( x − τ ) d τ = ∑ i = − ∞ + ∞ ∑ j = − ∞ + ∞ f j g i − j x j f*g(x)=\int_{-\infty}^{+\infty}f(\tau)g(x-\tau){\rm d}\tau=\sum_{i=-\infty}^{+\infty}\sum_{j=-\infty}^{+\infty}f_jg_{i-j}x^j fg(x)=+f(τ)g(xτ)dτ=i=+j=+fjgijxj

不难看出,我们日常使用的多项式乘积运算本质对应于多项式卷积运算,而乘积其实实际上是各项系数一一相乘。之所以通常的乘积其实是用卷积算出来的,是因为数字的乘积本质是一个 x = 10 x=10 x=10 的多项式卷积——如 232 × 147 = ( 2 x 2 + 3 x + 2 ) ( x 2 + 4 x + 7 ) ∣ x = 10 232\times 147=(2x^2+3x+2)(x^2+4x+7)|_{x=10} 232×147=(2x2+3x+2)(x2+4x+7)x=10,而且 x k x^k xk 系数得到也是通过 ∑ i = 0 k x i x k − i \displaystyle \sum_{i=0}^k x^ix^{k-i} i=0kxixki 逐项乘起来然后相加得到的。

你可能感兴趣的:(算法)