浙工商 2026 暑期内部排位赛 2 部分题解

F 昵称

标签

组合数学,\(dp\).

题意

给定长度分别为 \(n,m\) 的两字符串 \(s,t\),其中 \(n\le m\),需要计算合法的 \((s',t')\) 数量,模 \(998244353\),约束如下:

  • \(s'\)\(s\) 的一个排列.

  • \(t'\)\(t\) 删掉 \(m-n\) 个字符后的排列,且 \(t'_i = s'_i\)\(t'_i = s'_i+1\).

\(1\le n \le m \le 2\times 10^5\).

思路

\(a_c,b_c\) 分别为 \(s,t\) 中字符 \(c\) 的数量,\(x_c\) 为 字符 \(c\) 中匹配 \(c+1\) 的数量.

先不考虑 \(x\),方案数是:

\[\frac{n!}{\prod_{c=0}^{25}{a_c!}} \]

\(a_c\)\(c\) 中替换 \(x_c\) 个为 \(c+1\)

\[\frac{n!}{\prod_{c=0}^{25}{a_c!}}\cdot \prod_{c=0}^{25}{\binom{a_c}{x_c}} \]

统计每组合法的 \(x\) 的贡献,即计算:

\[\sum_{x}{\prod_{c=0}^{25}{\binom{a_c}{x_c}}} \]

接下来分析 \(x\) 的约束,对于 \(t\) 中字符 \(c\),需要匹配 \(a_c-x_c\)\(c\)\(x_{c-1}\)\(c-1\),因此需要满足 \(b_c \ge a_c-x_c+x_{c-1}\),即 \(x_c \ge x_{c-1}+a_c-b_c\),那么可以考虑 \(dp\).

\(dp[c][x]\) 为处理完 \(0\cdots c\) 的字符,\(x_c=x\) 的贡献,\(x_{c-1}\) 可以转移到 \([x_c+b_c-a_c,n]\),这个后缀形式统一转移到 \([x_c+b_c-a_c]\),之后求前缀和即可,最终答案是 \(dp[25][0]\),因为 \(x_{25}\) 必须是 \(0\).

时间复杂度 \(\mathcal{O}(26n)\).

代码

//author:kzssCCC

#include <bits/stdc++.h>
using namespace std;
using ll = long long;

template<int MOD>
struct modint {
    int val;
    
    modint() : val(0) {}
    modint(long long v) {
        val = v % MOD;
        if (val < 0) val += MOD;
    }
    
    modint& operator++() { val = (val + 1 == MOD ? 0 : val + 1); return *this; }
    modint& operator--() { val = (val == 0 ? MOD - 1 : val - 1); return *this; }
    modint operator++(int) { modint res = *this; ++*this; return res; }
    modint operator--(int) { modint res = *this; --*this; return res; }
    
    modint& operator+=(const modint& o) { val += o.val; if (val >= MOD) val -= MOD; return *this; }
    modint& operator-=(const modint& o) { val -= o.val; if (val < 0) val += MOD; return *this; }
    modint& operator*=(const modint& o) { val = 1LL * val * o.val % MOD; return *this; }
    modint& operator/=(const modint& o) { return *this *= o.inv(); }
    
    friend modint operator+(modint a, const modint& b) { return a += b; }
    friend modint operator-(modint a, const modint& b) { return a -= b; }
    friend modint operator*(modint a, const modint& b) { return a *= b; }
    friend modint operator/(modint a, const modint& b) { return a /= b; }

    friend bool operator==(const modint& a, const modint& b) { return a.val == b.val; }
    friend bool operator!=(const modint& a, const modint& b) { return a.val != b.val; }
    modint operator-() const { modint res = *this; res.val = (res.val == 0 ? 0 : MOD - res.val); return res;};
    modint operator+() const { return *this; };
    
    modint qpow(long long p) const {
        modint res = 1, a = *this;
        while (p > 0) {
            if (p & 1) res *= a;
            a *= a;
            p >>= 1;
        }
        return res;
    }

    modint inv() const {
        return qpow(MOD - 2);
    }
    
    friend std::ostream& operator<<(std::ostream& os, const modint& m) { return os << m.val; }
    friend std::istream& operator>>(std::istream& is, modint& m) { long long v; is >> v; m = modint(v); return is; }
};
using Z = modint<998244353>;

struct binom{
    int n;
    vector<Z> fac,inv;

    binom(int _n){
        n = _n;
        fac.assign(n+1,0);
        inv.assign(n+1,0);
        
        fac[0] = 1;
        for (int i=1;i<=n;i++){
            fac[i] = fac[i-1]*i;
        }       
        inv[n] = fac[n].inv();
        for (int i=n-1;i>=0;i--){
            inv[i] = inv[i+1]*(i+1);
        }
    }

    Z C(int a,int b){
        if (a<0 || a>n || b<0 || b>n || a<b) return 0;
        return fac[a]*inv[b]*inv[a-b];
    }

    Z A(int a,int b){
        if (a<0 || a>n || b<0 || b>n || a<b) return 0;
        return fac[a]*inv[a-b];
    }
};

binom bn(2e5);

void solve(){
    int n,m;
    cin >> n >> m;

    string s,t;
    cin >> s >> t;
    s = ' '+s;
    t = ' '+t;

    vector<int> a(26),b(26);
    for (int i=1;i<=n;i++){
        a[s[i]-'A']++;
    }

    for (int i=1;i<=m;i++){
        b[t[i]-'A']++;
    }

    Z res = bn.fac[n];
    for (int c=0;c<26;c++){
        res /= bn.fac[a[c]];
    }

    vector<vector<Z>> dp(27,vector<Z>(n+1));
    dp[0][0] = 1;
    for (int c=0;c<26;c++){
        for (int x=0;x<=n;x++){
            if (dp[c][x]==0) continue;
            if (x+a[c]-b[c]<=a[c]){
                dp[c+1][max(0,x+a[c]-b[c])] += dp[c][x];
            }
        }

        for (int x=1;x<=a[c];x++){
            dp[c+1][x] += dp[c+1][x-1];
        }   

        for (int x=0;x<=a[c];x++){
            dp[c+1][x] *= bn.C(a[c],x);
        }
    }

    res *= dp[26][0];
    cout << res << '\n';
}

int main(){
    ios::sync_with_stdio(false);
    cin.tie(0);
    
    int t = 1;
    // cin >> t;
    while (t--) solve();

    return 0;
}

H 方格游戏

标签

博弈论,欧几里得算法.

题意

一张 \([1,10^9] \times [1,10^9]\) 网格,给定 \(n\) 个黑点,其他全白.

每次操作选择一个黑点 \((r,c)\),对所有满足 \(0\le x,y \le \min(r,c)\) 的格子 \((r-x,c-y)\) 翻转颜色,无法操作者输,问胜者是先手还是后手.

\(1\le n \le 2\times 10^5\).

思路

难以分析每次操作的影响,考虑寻找每次操作的不变量.

博弈问题中,可以尝试寻找一个量 \(S\),使得每次操作 \(S := S \oplus 1\).

具体地,给初始每个黑点钦定一个值 \(0/1\),其余白点均为 \(0\),定义 \(S\) 为整张表格的异或值,因此要让一次操作覆盖的网格内异或值为 \(1\).

\(f[i][j]\) 为网格的权值,\(pre[i][j]\)\(f[i][j]\) 的二维前缀异或和,对一次操作讨论:

  • \(r\ge c\),要求 \(pre[r][c]\oplus pre[r-c][c]=1\).

  • \(r\lt c\),要求 \(pre[r][c]\oplus pre[r][c-r]=1\).

可以发现,这是辗转相减的形式,可以递推求得所有 \(pre[i][j]\) 的值,需要用欧几里得算法加速这个过程.

时间复杂度 \(\mathcal{O}(n\log V)\)\(V\) 是值域.

代码

//author:kzssCCC

#include <bits/stdc++.h>
using namespace std;
using ll = long long;

int euclid(int a,int b){
    if (b==0) return 0;
    return ((a/b)&1)^euclid(b,a%b);
}

int cal(int x,int y){
    return euclid(x,y)^euclid(x-1,y)^euclid(x,y-1)^euclid(x-1,y-1);
}

void solve(){
    int n;
    cin >> n;

    int res = 0;
    for (int i=1;i<=n;i++){
        int x,y;
        cin >> x >> y;
        res ^= cal(x,y);
    }

    if (res&1){
        cout << "FIRST" << '\n';
    }
    else{
        cout << "SECOND" << '\n';
    }
}

int main(){
    ios::sync_with_stdio(false);
    cin.tie(0);
    
    int t = 1;
    // cin >> t;
    while (t--) solve();

    return 0;
}

I 增加线缆

标签

网络流,最小路径覆盖.

题意

给定 \(n-1\) 条有向边,保证忽略方向后构成图为树,要求添加最少的边使得图的拓扑序唯一,给出构造方案.

\(2\le n \le 10^5\).

思路

拓扑序唯一意味着拓扑序相邻两点必有边,否则可以交换位置.

要求添加边最少,需要尽可能多地保留原图的边,分析可知,这相当于在原图中保留一些路径,因此可以总结为最小路径覆盖问题,可以用二分图匹配解决.

跑完二分图匹配,需要连接所有路径,将保留路径缩成单点并记录起点和终点,对缩点图拓扑排序后,按拓扑序依次连接路径起点和终点即可.

时间复杂度 \(\mathcal{O}(n\sqrt{n})\).

代码

//author:kzssCCC

#include <bits/stdc++.h>
using namespace std;
using ll = long long;

class dinic{
public:
    const ll INF = 9e18;
    int n,s,t;
    vector<vector<array<ll,4>>> adj;
    ll mxf;
    vector<int> cur,depth;

    dinic(int _n,int _s,int _t){
        n = _n;
        s = _s;
        t = _t;
        adj = vector<vector<array<ll,4>>>(n+1);
    }

    void add(int u,int v,ll w){
        adj[u].push_back({w,1,(int)adj[v].size(),v});
        adj[v].push_back({0,0,(int)adj[u].size()-1,u});
    }

    bool bfs(){
        depth = vector<int>(n+1,-1);
        queue<int> q;
        depth[s] = 0;
        q.push(s);

        while (!q.empty()){
            int u = q.front();
            q.pop();

            for (auto& [w,flag,rev,v]:adj[u]){
                if (w>0 && depth[v]==-1){
                    depth[v] = depth[u]+1;
                    q.push(v);
                }
            }
        }

        return depth[t]!=-1;
    }

    ll dfs(int u,ll mf){
        if (u==t) return mf;
        ll sum = 0;
        int len = adj[u].size();

        for (int& i=cur[u];i<len;i++){
            auto& [w,flag,rev,v] = adj[u][i];
            if (w>0 && depth[v]==depth[u]+1){
                ll f = dfs(v,min(mf,w));
                sum += f;
                mf -= f;
                w -= f;
                adj[v][rev][0] += f;

                if (mf==0) break;
            }
        }

        return sum;
    }

    void work(){
        mxf = 0;
        while (bfs()){
            cur = vector<int>(n+1);
            mxf += dfs(s,INF);
        }
    }
};

void solve(){
    int n;
    cin >> n;

    vector<vector<int>> adj(n+1);
    for (int i=0;i<n-1;i++){
        int u,v;
        cin >> u >> v;
        adj[u].push_back(v);
    }   

    int s = 2*n+1;
    int t = s+1;
    dinic dn(2*n+2,s,t);  

    for (int u=1;u<=n;u++){
        for (auto& v:adj[u]){
            dn.add(u,v+n,1);
        }
        dn.add(s,u,1);
        dn.add(u+n,t,1);
    }

    dn.work();
    cout << n-1-dn.mxf << '\n';

    vector<int> pre(n+1,-1),next(n+1,-1);
    for (int u=1;u<=n;u++){
        for (auto& [w,op,rev,v]:dn.adj[u]){
            if (op && w==0){
                next[u] = v-n;
                pre[v-n] = u;   
            }
        }
    }   

    vector<pair<int,int>> wait{{-1,-1}};
    vector<int> belong(n+1);

    for (int i=1;i<=n;i++){
        if (pre[i]==-1){
            int u = i;
            int id = wait.size();
            while (next[u]!=-1){
                belong[u] = id;
                u = next[u];
            }
            belong[u] = id;
            wait.emplace_back(i,u);
        }       
    }

    int m = wait.size()-1;
    vector<vector<int>> g(m+1);
    for (int u=1;u<=n;u++){
        for (auto& v:adj[u]){
            if (belong[u]!=belong[v]){
                g[belong[u]].push_back(belong[v]);
            }
        }
    } 

    vector<int> ing(m+1);
    for (int i=1;i<=m;i++){
        sort(g[i].begin(),g[i].end());
        g[i].erase(unique(g[i].begin(),g[i].end()),g[i].end());
        for (auto& v:g[i]){
            ing[v]++;
        }
    }

    queue<int> q;
    for (int i=1;i<=m;i++){
        if (ing[i]==0){
            q.push(i);
        }
    }

    vector<int> topo{-1};
    while (!q.empty()){
        int u = q.front();
        q.pop();
        topo.push_back(u);

        for (auto& v:g[u]){
            if (--ing[v]==0){
                q.push(v);
            }
        }
    }

    for (int i=2;i<=m;i++){
        cout << wait[topo[i-1]].second << ' ' << wait[topo[i]].first << '\n';
    }
}

int main(){
    ios::sync_with_stdio(false);
    cin.tie(0);
    
    int t = 1;
    // cin >> t;
    while (t--) solve();

    return 0;
}

J 还原词典

标签

\(sa\)\(st\) 表,\(dp\).

题意

给定长度为 \(n\) 字符串 \(s\),将 \(s\) 划分成若干连续子串且满足子串严格递增,求最大划分数量并给出方案.

\(1\le n \le 5000\).

思路

考虑 \(dp\),令 \(dp[i][j]\) 为以 \(i\) 为起点长度为 \(j\) 的子串作为最后一个子串能达到的最大划分数量.

\(r\) 为第一个满足 \(s[i\cdots i+j-1] \lt s[i+j,i+j+r-1]\) 的长度,那么 \(dp[i+j][R]\leftarrow dp[i][j]+1\)\(R\ge r\),转移到 \(dp[i+j][r]\) 后求前缀最大值即可,问题转化为求 \(r\).

发现起点是固定的,考虑求 \(lcp\),可以用 \(st\) 表维护 \(sa\)\(height\) 数组快速解决,分类讨论转移即可.

时间复杂度 \(\mathcal{O}(n^2)\).

代码

//author:kzssCCC

#include <bits/stdc++.h>
using namespace std;
using ll = long long;

const int INF = 1e9;

void solve(){
    string s;
    cin >> s;
    int n = s.size();   
    s = ' '+s;

    vector<int> sa(n+1),rk(n+1),height(n+1);
    {
        int m = 255;
        vector<int> ord(n+1),cnt(max(n,m)+1);
        for (int i=1;i<=n;i++){
            rk[i] = (unsigned char)s[i];
            cnt[rk[i]]++;
        }

        for (int i=1;i<=m;i++){
            cnt[i] += cnt[i-1];
        }

        for (int i=n;i>=1;i--){
            sa[cnt[rk[i]]--] = i;
        }

        for (int k=1;;k<<=1){
            int p = 1;
            for (int i=n-k+1;i<=n;i++){
                ord[p++] = i;
            }   

            for (int i=1;i<=n;i++){
                if (sa[i]>k){
                    ord[p++] = sa[i]-k;
                }
            }   

            fill(cnt.begin(),cnt.begin()+m+1,0);
            for (int i=1;i<=n;i++){
                cnt[rk[ord[i]]]++;
            }        

            for (int i=1;i<=m;i++){
                cnt[i] += cnt[i-1];
            }

            for (int i=n;i>=1;i--){
                sa[cnt[rk[ord[i]]]--] = ord[i];
            }

            swap(ord,rk);
            rk[sa[1]] = 1;
            for (int i=2;i<=n;i++){
                pair<int,int> p1 = {ord[sa[i-1]],sa[i-1]+k<=n?ord[sa[i-1]+k]:-1};
                pair<int,int> p2 = {ord[sa[i]],sa[i]+k<=n?ord[sa[i]+k]:-1};
                rk[sa[i]] = rk[sa[i-1]]+(p1<p2);
            }

            m = rk[sa[n]];
            if (m==n) break;            
        }

        int k = 0;
        for (int i=1;i<=n;i++){
            if (rk[i]==1) continue;
            int j = sa[rk[i]-1];
            while (i+k<=n && j+k<=n && s[i+k]==s[j+k]){
                k++;
            }

            height[rk[i]] = k;
            k = max(0,k-1);
        }
    }

    vector<int> lg(n+1);
    for (int i=2;i<=n;i++){
        lg[i] = lg[i>>1]+1;
    }

    int K = lg[n];
    vector<vector<int>> st(n+1,vector<int>(K+1));
    for (int i=1;i<=n;i++){
        st[i][0] = height[i];
    }

    for (int k=1;k<=K;k++){
        int len = 1<<k;
        int half = len>>1;
        for (int i=1;i+len-1<=n;i++){
            st[i][k] = min(st[i][k-1],st[i+half][k-1]);
        }
    }

    auto query = [&](int l,int r){
        int len = r-l+1;
        int k = lg[len];
        return min(st[l][k],st[r-(1<<k)+1][k]);
    };

    vector<vector<int>> dp(n+1,vector<int>(n+1,-INF));
    dp[1][1] = 1;

    for (int i=1;i<=n;i++){
        for (int j=1;j<=n;j++){
            dp[i][j] = max(dp[i][j],dp[i][j-1]);
            
            if (i+j<=n && dp[i][j]!=-INF){
                int r = query(min(rk[i],rk[i+j])+1,max(rk[i],rk[i+j]));
                if (r>=j){
                    if (i+2*j<=n){
                        dp[i+j][j+1] = max(dp[i+j][j+1],dp[i][j]+1);
                    }
                } 
                else{
                    if (i+j+r<=n && s[i+r]<s[i+j+r]){
                        dp[i+j][r+1] = max(dp[i+j][r+1],dp[i][j]+1);
                    }
                }
            }
        }
    }

    int mx = 1;
    for (int i=1;i<=n;i++){
        mx = max(mx,dp[i][n-i+1]);
    }
    cout << mx << '\n';

    vector<string> res{string(n+5,'Z')};
    int R = n;
    mx++;
    while (R>=1){
        int pos = -1;
        for (int i=1;i<=R;i++){
            if (dp[i][R-i+1]==mx-1 && s.substr(i,R-i+1)<res.back()){
                pos = i;
                break;
            }
        }

        res.push_back(s.substr(pos,R-pos+1));
        mx--;
        R = pos-1;
    }

    reverse(res.begin(),res.end());
    res.pop_back();
    for (auto& v:res){
        cout << v << '\n';
    }
}

int main(){
    ios::sync_with_stdio(false);
    cin.tie(0);
    
    int t = 1;
    // cin >> t;
    while (t--) solve();

    return 0;
}
posted @ 2026-08-04 13:25  kzssCCC  阅读(10)  评论(0)    收藏  举报