2022 CCPC Henan Provincial Collegiate Programming Contest 题解

AE

签到秒了

DL

So Hard 做不了一点

I

模拟还是算了

B

dp 的优化

没有解题思路, 陈的观察加刘的码力直接一发过了

CODE
#include<bits/stdc++.h>
using namespace std;
#define int long long 
const int mod=998244353;
#define IOS ios::sync_with_stdio(false);cin.tie(nullptr);cout.tie(nullptr);

void solve() {
    int n;
    string s;
    cin>>s;
    n=s.length();
    s=" "+s+s;
    
    map<char,int>mp;
    mp['a']=1;
    mp['e']=2;
    mp['h']=3;
    mp['n']=4;
    int ans=0;
    for(int k=1;k<=min(15ll,n);k++){
            vector<int>dp(2*n+1,0);
            int m=min(n,k);
        for(int i=m;i<m+n;i++){
                int cur=0;
                int d=1;
                for(int j=i;j>=max(m,i-15);j--){
                        cur=(cur+mp[s[j]]*d)%mod;
                        d*=31;
                        dp[i]=max(dp[j-1]+cur,dp[i]);
                }
                //if(m==5) cout<<i<<": "<<dp[i]<<"\n";


            }
            //cout<<dp[m+n-1]<<'\n';
            ans=max(dp[n+m-1],ans);
    }
    
    cout<<ans<<"\n";

}
signed main() {
    IOS;
    
    int t = 1;
    //cin >> t;

    while (t--) {
        solve();
    }
}

C

线段树

解题思路

先不考虑修改,只考虑对于一个特定的区间,我们如何快速地去回答任意给定的 \(k\)
将一个区间划分成 \(K\) 段就是一个隔板法。但此处我们有限制,对于 \(S_{i + 1} \leq S_i\) 的位置,我们必须插入一个隔板,否则就是不合法的。所以对于一个区间,我们只要知道了该区间有多少个位置必须要插入隔板,我们就可以快速回答任意的 \(K\):假设区间中有 \(N\) 个空位,\(X\) 个必须要插入隔板的位置,则答案就是

\[C_{N - X}^{(K - 1) - X} \]

(只用特判掉一些答案为 0 的情况就好了。)

而且我们发现,对于两个区间的合并也是比较简单的:只用将两个区间的 \(X\) 相加,然后判断下两个区间合并是否会产生新的必须要隔板的位置就好了:

// std::array<int, 3> node 表示一个区间中要维护的信息
// node[0] 代表最左边的字母
// node[2] 代表最右边的字母
// node[1] 代表有多少个必须插入隔板的位置
std::array<int, 3> operator+(const std::array<int, 3>& l, const std::array<int, 3>& r) {
    return { l[0], l[1] + r[1] + (l[2] >= r[0]), r[2] };
}

接下来只需要解决如何修改,我们就可以用线段树来实现了。

为了方便处理修改,首先把 ABCD 变成 0123,这样修改一次区间中的数 \(x\) 就变成 \((x + 1) \% 4\)

我们只用维护必插位置的数量。而且手玩一下就可以发现,只有使区间端处的 3 变成 0 的修改,才有可能改变必插位置的数量。具体的若最前面的是 3 则修改后少一个,若最后面是 3 则修改后多一个。

于是,上述修改方法配合 lazy tag 我们就可以实现区间修改了:

// cur 是要修改的区间的编号
// c 是修改的次数
void move(int cur, int c) {
    (tag[cur] += c) %= 4;
    (tr[cur][0] += c) %= 4;
    (tr[cur][2] += c) %= 4;
    // 小于 c 说明有一个从 3 到 0 的过程
    tr[cur][1] += (tr[cur][2] < c) - (tr[cur][0] < c);
}

对于组合数,预处理以下阶乘和阶乘逆元就好了(main 函数里面)

CODE
constexpr int N = 1e5, Q = 1e5, Inf = 1e9;

int a[N];

std::array<int, 3> tr[N << 2];
int tag[N << 2];
i64 fac[N + 5], invfac[N + 5];

i64 qpow(i64 a, i64 p) {
    i64 res = 1;
    while (p) {
        if (p & 1) {
            (res *= a) %= Mod;
        }
        (a *= a) %= Mod;
        p >>= 1;
    }
    return res;
}
i64 inv(i64 a) {
    return qpow(a, Mod - 2);
}
i64 C(int n, int m) {
    return fac[n] * invfac[m] % Mod * invfac[n - m] % Mod;
}

std::array<int, 3> operator+(const std::array<int, 3>& l, const std::array<int, 3>& r) {
    return { l[0], l[1] + r[1] + (l[2] >= r[0]), r[2] };
}

void push_up(int cur) {
    tr[cur] = tr[cur << 1] + tr[cur << 1 | 1];
}

void move(int cur, int c) {
    (tag[cur] += c) %= 4;
    (tr[cur][0] += c) %= 4;
    (tr[cur][2] += c) %= 4;
    tr[cur][1] += (tr[cur][2] < c) - (tr[cur][0] < c);
}

void push_down(int cur) {
    move(cur << 1, tag[cur]);
    move(cur << 1 | 1, tag[cur]);
    tag[cur] = 0;
    return;
}

void build(int cur, int l, int r) {
    if (l + 1 == r) {
        tr[cur][0] = tr[cur][2] = a[l];
        tr[cur][1] = 0;
        return;
    }

    int m = l + r >> 1;
    build(cur << 1, l, m);
    build(cur << 1 | 1, m, r);
    push_up(cur);
}
void upd(int cur, int l, int r, int sl, int sr) {
    if (sl <= l && r <= sr) {
        move(cur, 1);
        return;
    }

    if (tag[cur]) {
        push_down(cur);
    }
    int m = l + r >> 1;
    if (m > sl) {
        upd(cur << 1, l, m, sl, sr);
    }
    if (m < sr) {
        upd(cur << 1 | 1, m, r, sl, sr);
    }
    push_up(cur);
}

std::array<int, 3> quiry(int cur, int l, int r, int sl, int sr) {
    if (sl <= l && r <= sr) {
        return tr[cur];
    }

    if (tag[cur]) {
        push_down(cur);
    }
    int m = l + r >> 1;
    if (m >= sr) {
        return quiry(cur << 1, l, m, sl, sr);
    }
    else if (m <= sl) {
        return quiry(cur << 1 | 1, m, r, sl, sr);
    }
    else {
        return quiry(cur << 1, l, m, sl, sr) + quiry(cur << 1 | 1, m, r, sl, sr);
    }
}

void solve()
{
    int n = 0, q = 0;
    std::string s;
    std::cin >> n >> q >> s;
    for (int i = 0; i < n; i++) {
        a[i] = s[i] - 'A';
    }

    build (1, 0, n);
    while (q--) {
        int op = 0;
        int l = 0, r = 0;
        std::cin >> op >> l >> r;
        l--;
        if (op == 1) {
            upd(1, 0, n, l, r);
        }
        else {
            int k = 0;
            std::cin >> k;
            int c = quiry(1, 0, n, l, r)[1];

            if (k <= c || k > r - l) {
                std::cout << 0 << '\n';
            }
            else {
                std::cout << C(r - l - 1 - c, k - 1 - c) << '\n';
            }
        }
    }
}

int main()
{
    IOS;
    int _t = 1;
    
    fac[0] = 1;
    for (int i = 1; i <= N; i++) {
        fac[i] = 1ll * i * fac[i -  1] % Mod;
    }
    invfac[N] = inv(fac[N]);
    for (int i = N; i >= 1; i--) {
        invfac[i - 1] = 1ll * i * invfac[i] % Mod;
    }

    while (_t--)
    {
        solve();
    }

    sp();

    return 0;
}

F

构造 观察

解题思路

对于奇数的情况是好处理的,而偶数,最小只能构造出 6 的情况:134,对于大于 6 的偶数在后面添连续的相邻的数就好了。 2 和 4 是真构造不出来。

CODE
void solve()
{
    int n = 0;
    std::cin >> n;
    if (n & 1) {
        n = n + 1 >> 1;
        std::cout << n << '\n';
        for (int i = 1; i <= n; i++) {
            std::cout << i << ' ';
        }
    }
    else if (n > 4) {
        n >>= 1;
        std::cout << n << '\n';
        n++;
        std::cout << 1 << ' ';
        for (int i = 3; i <= n; i++) {
            std::cout << i << ' ';
        }
    }
    else {
        std::cout << -1;
    }
    std::cout << '\n';
    return;
}

G

阅读理解题

解题思路

说是阅读理解题是因为只要理解好题意你就会发现什么什么概率都是诈骗。

CODE
void solve()
{
    int n = 0, m = 0;
    std::cin >> n >> m;
    for (int i = 0; i < m; i++) {
        cnt[i] = 1;
    }
    std::string s;
    for (int i = 0; i < n; i++) {
        std::cin >> s;
        for (int j = 0; j < m; j++) {
            int c = s[j] - '0';
            if (c == 0) {
                cnt[j] = 0;
            }
        }
    }

    int ans = 0;
    for (int i = 0; i < m; i++) {
        if (cnt[i]) {
            ans++;
        }
    }
    std::cout << ans << '\n';
}

H

搜索 暴力

解题思路

按每根管子的形状爆搜就好了。

CODE
bool dfs(int dir, int cx, int cy) {
    if (cx == 2 && cy == y) {
        return true;
    }
    if (cx < 0 || cx > 1 || cy < 0 || cy >= m) {
        return false;
    }
    if (vis[cx][cy]) {
        return false;
    }

    vis[cx][cy] = true;
    bool res = false;
    if (g[cx][cy] == 'I') {
        res = dfs(dir, cx + dx[dir], cy + dy[dir]);
    }
    else {
        res = dfs((dir + 1) % 4, cx + dx[(dir + 1) % 4], cy + dy[(dir + 1) % 4]);
        if (not res) {
            res = dfs((dir + 3) % 4, cx + dx[(dir + 3) % 4], cy + dy[(dir + 3) % 4]);
        }
    }
    vis[cx][cy] = false;
    return res;
}

void solve()
{
    std::cin >> m >> x >> y;
    x--, y--;

    for (int i = 0; i < 2; i++) {
        for (int j = 0; j < m; j++) {
            vis[i][j] = false;
        }
    }
    for (int i = 0; i < 2; i++) {
        std::cin >> g[i];
    }

    std::cout << (dfs(2, 0, x) ? "YES" : "NO") << '\n';
    return;
}

J

MEX 观察

解题思路

写的时候没想清楚写了一坨启发式合并然后 T 了。

直接以权值为 0 的那个点为根往下 dfs 就好了,顺便维护每颗子树的大小和子树中权值最小的是多少。

对于 MEX 为 0 的,就是在于根相邻的子树里找一颗最大的。对于其他 MEX 值 \(x\),先找到权值与该 MEX 值相等的点 \(u\),首先可以判断,MEX 为 \(x\) 的连通块若存在,则必然在点 \(u\) 的上面(因为权值为 0 的点在上面),如何判断存不存在呢?只用看以 \(u\) 为根的子树中节点的最小值是多少就好了,只有当等于 \(x\) 时才存在。

CODE
int n = 0, rt = 0;
int v[N + 5], siz[N + 5], mn[N + 5], ans[N + 5];
std::vector<int> g[N + 5];

void dfs(int cur, int fa) {
    siz[cur] = 1;
    mn[cur] = v[cur];
    for (auto &to : g[cur]) {
        if (to == fa) {
            continue;
        }

        dfs(to, cur);
        siz[cur] += siz[to];
        mn[cur] = std::min(mn[cur], mn[to]);
    }

    if (mn[cur] == v[cur]) {
        ans[v[cur]] = n - siz[cur];
    }
}

void solve()
{
    std::cin >> n;
    for (int i = 1; i <= n; i++) {
        std::cin >> v[i];
        if (v[i] == 0) {
            rt = i;
        }
    }
    for (int i = 2; i <= n; i++) {
        int u = 0;
        std::cin >> u;
        g[u].push_back(i);
        g[i].push_back(u);
    }

    dfs(rt, 0);
    int mx = 0;
    for (auto &to : g[rt]) {
        mx = std::max(mx, siz[to]);
    }
    std::cout << mx << ' ';
    for (int i = 1; i < n; i++) {
        std::cout << ans[i] << ' ';
    }
    std::cout << n << '\n';
    return;
}

K

基环树

解题思路

首先根据图建立基环树森林。

最重要的观察就是,在基环树森林中,长度不同的环的个数的数量级是 \(\sqrt{n}\) 的,因为:\(\sum_i^k = {k(k + 1) \over 2}\)

根据这一点,我们将所有长度相同的基环树的信息维护到一起,这样就可以节省大量时间空间。

然后对于基环树上的点,在它到达环之前是一定不断变化的,到达环之后就呈现周期性变化,这个周期只与环长有关(这也是为什么上面将环长相同的树的信息合并到一起)。

然后用个前缀和再特判以下 \(a=b\) 的情况就好了。

CODE
constexpr int N = 1e5, L = 450; // 450 sqrt(2e5)
int to[N + 5], in[N + 5], d[N + 5];
int pre[L][N + 5], len[L], col[N + 5], cnt, idx[N + 5];

void dfs(int cur) {
    if (col[cur] == 0) {
        dfs(to[cur]);
        col[cur] =  col[to[cur]]; // 染色
        d[cur] = d[to[cur]] + 1;
        pre[col[cur]][d[cur]]++;
    }
    return;
}

void solve()
{
    int n = 0;
    std::cin >> n;
    for (int i = 1; i <= n; i++) {
        int f = 0;
        std::cin >> f;
        to[i] = f;
        in[f]++;
    }

    std::queue<int> q;
    for (int i = 1; i <= n; i++) {
        if (in[i] == 0) {
            q.push(i);
        }
    }

    while (not q.empty()) {
        int cur = q.front();
        q.pop();
        if (--in[to[cur]] == 0) {
            q.push(to[cur]);
        }
    }

    // 维护环
    for (int i = 1; i <= n; i++) {
        if (in[i] != 0) {
            // 确定颜色,环长相同的基环树颜色相同
            int cur = i, l = 0;
            do {
                l++;
                in[cur] = 0;
                d[cur] = 0;
                cur = to[cur];
            } while (cur != i);
            int c = idx[l];
            if (c == 0) {
                c = idx[l] = ++cnt;
                len[c] = l;
            }

            pre[c][0] += l;
            do
            {
                col[cur] = c;
                cur = to[cur];
            } while (cur != i);
        }
    }

    for (int i = 1; i <= n; i++) {
        if (col[i] == 0) {
            dfs(i);
        }
    }
    for (int i = 1; i <= cnt; i++) {
        for (int j = 1; j <= n; j++) {
            pre[i][j] += pre[i][j - 1];
        }
    }

    int qr = 0;
    std::cin >> qr;
    while (qr--) {
        i64 a = 0, b = 0;
        std::cin >> a >> b;

        if (a == b) {
            std::cout << n << '\n';
            continue;
        }
        int ans = 0;
        for (int l = 1; l <= cnt; l++) {
            if (a % len[l] == b % len[l]) {
                ans += pre[l][std::min({ a, b, i64(n) })];
            }
        }
        std::cout << ans << '\n';
    }
    return;
}
posted @ 2025-03-28 15:54  Young_Cloud  阅读(47)  评论(0)    收藏  举报