第十一届中国大学生程序设计竞赛 济南站(CCPC 2025 Jinan Site)

Preface

还是上上周的 CCPC 济南,当时场外看同校的 UESTC_有无名队 拿下亚军(可惜最后几分钟被 PKU 反杀了,不然真冠军了),也对这场比赛的毒瘤程度早有耳闻

实际打了之后发现前中期题的 GAP 非常大,A 题感觉很不 match 我们队的知识点,导致赛后半小时才搞出来

第五题的 E 和第四题的 F 之间也卡了两个多小时,总而言之就是做大牢,最后也是连金线都摸不到,苦露西


A. 暗语

神秘 Crypto 题,然而我们队纯靠打表找规律硬做的这个题

需要注意到循环节和 __builtin_ctz 有关,还要猜出从模 \(2^{p-1}\) 扩展到模 \(2^p\) 的增量,我只能说这题是真有点 ex 的

唉,好羡慕数学大手子

#include <bits/stdc++.h>

using llui = __int128_t;

std::istream& operator >>(std::istream& in, llui &a) {
    long long unsigned int _a; in >> _a; a = _a;
    return in;
}

std::ostream& operator <<(std::ostream& out, llui a) {
    long long unsigned int _a = a; out << _a;
    return out;
}

llui get_mask(llui mask_len) {
    // if(mask_len >= 64) return -llui(1);
    return (llui(1) << mask_len) - llui(1);
}

llui ksm(llui a, llui b, llui mask_len = 64) {
    llui c = llui(1);
    while(b) {
        if(b & llui(1)) c = c * a;
        a = a * a;
        b >>= llui(1);
    }
    if(mask_len == 64) return c;
    return c & ((llui(1) << mask_len) - llui(1));
}

std::optional<llui> inv(llui a, llui p) {
    if(a % 2 == 0) return std::nullopt;
    return ksm(a, (llui(1) << (p - llui(1))) - llui(1), p);
}

std::optional<llui> solve(llui a, llui b, llui p) {
    auto debug = [&](llui ans) {
        // std::cerr << "solve(" << a << ", " << b << ", " << p << ") = " << ans << char(10);
        return ans;
    };
    if(b % (llui(1) << p) == 1) return debug(0ull);
    if(a % 2 == 0 || p <= 3) {
        llui A = 1, x = 0;
        while(x <= 64) {
            if(((A - b) & get_mask(p)) == 0) return debug(x);
            A *= a, x += llui(1);
        }
        return std::nullopt;
    }
    if(b % 2 == 0) return std::nullopt;
    if(p == 1) return debug(llui(1));
    // if(((a - b) & get_mask(p)) == 0) return debug(llui(1));
    auto _x = solve(a, b, p - 1);
    if(!_x) return std::nullopt;

    llui P = (llui(1) << p - 2 - __builtin_ctzll((a % (llui(1) << p) + 1) / 4));
    if(a == 1) P = 1;
    if(a == get_mask(p)) P = 2;
    
    llui x = *_x;
    if(P != 1) x %= P;

    if(((ksm(a, x, p) - b) & get_mask(p)) == 0ull) return debug(x);
    llui nx = (x < P / 2 ? x + P / 2 : x - P / 2);
    if(((ksm(a, nx, p) - b) & get_mask(p)) == 0ull) return debug(nx);
    // std::cerr << "x = " << x << ", a = " << (a & get_mask(p)) << ", b = " << (b & get_mask(p)) << ", P = " << P << char(10);
    return std::nullopt;
}

int main(void) {
    std::ios::sync_with_stdio(false);
    // for(llui a = 1; a <= 31; a += 2) {
    //     std::cerr << std::setw(2) << std::right << a << "(" << (a + 1) / 4 << ", " << (1 << 3 - __builtin_ctz((a + 1) / 4)) << "): ";
    //     for(llui b = 1; b <= 8; ++b)
    //         std::cerr << std::setw(2) << std::right << ksm(a, b, 5) << char(b == 8 ? 10 : 32);
    // }
    // for(llui a = 1; a <= 63; a += 2) {
    //     std::cerr << std::setw(2) << std::right << a << "(" << std::setw(2) << std::right << (a + 1) / 4 << ", " << std::setw(2) << std::right << (1 << 4 - __builtin_ctz((a + 1) / 4)) << "): ";
    //     for(llui b = 1; b <= 16; ++b)
    //         std::cerr << std::setw(2) << std::right << ksm(a, b, 6) << char(b == 16 ? 10 : 32);
    // }
    // return 0;
    // for(llui i = 1; i <= 63; i += 2) for(llui p = 1; p <= 64; ++p) {
    //     auto iv = inv(i, p);
    //     if(iv) std::cerr << *iv << "(" << (*iv * i & (p >= 64 ? -1 : (llui(1) << p) - 1)) << ")";
    //     else std::cerr << "N/A";
    //     std::cerr << char(p == 64 ? 10 : 32);
    // }
    int T; std::cin >> T; while(T--) {
        llui a, b;
        std::cin >> a >> b;
        auto ans = solve(a, b, 64);
        if(ans) std::cout << *ans << char(10);
        else std::cout << "broken message\n";
    }
    return 0;
}

C. 查找关键词

签到,按顺序匹配关键词序列,对于当前的每个值,找在上次值出现位置之后且最靠前的即可

实现时不需要二分,直接用单调性找即可

#include<cstdio>
#include<iostream>
#include<vector>
#include<algorithm>
#define RI register int
#define CI const int&
using namespace std;
const int N=1e6+5,M=15;
int m,b[M],t,n; vector <int> pos[M];
int main()
{
    scanf("%d",&m);
    for (RI i=1;i<=m;++i)
    scanf("%d",&b[i]);
    for (scanf("%d",&t);t;--t)
    {
        scanf("%d",&n);
        for (RI i=1;i<=m;++i) pos[i].clear();
        for (RI i=1;i<=n;++i)
        {
            int x; scanf("%d",&x);
            pos[x].push_back(i);
        }
        for (RI i=1;i<=m;++i)
        reverse(pos[i].begin(),pos[i].end());
        int ans=0;
        for (;;)
        {
            int lst=0,flag=1;
            for (RI i=1;i<=m;++i)
            {
                int x=b[i];
                while (!pos[x].empty()&&lst>pos[x].back()) pos[x].pop_back();
                if (pos[x].empty()) { flag=0; break; }
                lst=pos[x].back(); pos[x].pop_back();
            }
            if (!flag) break; ans+=flag;
        }
        printf("%d\n",ans);
    }
    return 0;
}

E. 二分图问题

考虑如果树上存在两个不交的连通块 \(S,T\),则它们之间一定存在一条路径(不包含端点),满足删去这条路径上的任意一个点/边后 \(S,T\) 分属不同连通块

刚开始想的是通过对不同长度的路径容斥进行处理,但后面发现非常难搞,遂考虑更进一步的转化

考虑这个题在删除一个点/边后的方案数是好计算的,而一条不包含两端点的路径满足边数减去点数恒等于 \(1\)

因此我们可以考虑用删去每条边后,在两部分找两个连通块的方案数,减去删去每个点后,找两个连通块的方案数

分析一下会发现此时只要对每个点求出 in[x],out[x] 表示子树内/外选同色点集的方案数即可,用 DSU on tree 即可快速统计

#include<cstdio>
#include<iostream>
#include<vector>
#define RI register int
#define CI const int&
using namespace std;
const int N=200005,mod=998244353;
int t,n,pw2[N],c[N],sz[N],son[N],in[N],out[N],all[N],bkt[N],IN,OUT; vector <int> v[N];
inline void inc(int& x,CI y)
{
    if ((x+=y)>=mod) x-=mod;
}
inline void dec(int& x,CI y)
{
    if ((x-=y)<0) x+=mod;
}
inline void DFS1(CI now=1,CI fa=0)
{
    sz[now]=1;
    for (auto to:v[now])
    {
        if (to==fa) continue;
        DFS1(to,now); sz[now]+=sz[to];
        if (sz[to]>sz[son[now]]) son[now]=to;
    }
}
inline void add(int x)
{
    x=c[x];
    dec(IN,pw2[bkt[x]]-1); dec(OUT,pw2[all[x]-bkt[x]]-1);
    ++bkt[x];
    inc(IN,pw2[bkt[x]]-1); inc(OUT,pw2[all[x]-bkt[x]]-1);
}
inline void del(int x)
{
    x=c[x];
    dec(IN,pw2[bkt[x]]-1); dec(OUT,pw2[all[x]-bkt[x]]-1);
    --bkt[x];
    inc(IN,pw2[bkt[x]]-1); inc(OUT,pw2[all[x]-bkt[x]]-1);
}
inline void travel_add(CI now,CI fa)
{
    add(now); for (auto to:v[now])
    if (to!=fa) travel_add(to,now);
}
inline void travel_del(CI now,CI fa)
{
    del(now); for (auto to:v[now])
    if (to!=fa) travel_del(to,now);
}
inline void DSU(CI now=1,CI fa=0,CI flag=1)
{
    for (auto to:v[now])
    {
        if (to==fa||to==son[now]) continue;
        DSU(to,now,0);
    }
    if (son[now]) DSU(son[now],now,1);
    for (auto to:v[now])
    {
        if (to==fa||to==son[now]) continue;
        travel_add(to,now);
    }
    add(now); in[now]=IN; out[now]=OUT;
    if (!flag) travel_del(now,fa);
}
inline void DFS2(int& ans,CI now=1,CI fa=0)
{
    if (now!=1) inc(ans,2LL*in[now]*out[now]%mod);
    int sum=out[now],quad=1LL*out[now]*out[now]%mod;
    for (auto to:v[now])
    {
        if (to==fa) continue;
        DFS2(ans,to,now);
        inc(sum,in[to]); inc(quad,1LL*in[to]*in[to]%mod);
    }
    sum=1LL*sum*sum%mod; dec(sum,quad);
    dec(ans,sum);
}
int main()
{
    pw2[0]=1;
    for (RI i=1;i<=200000;++i) pw2[i]=2LL*pw2[i-1]%mod;
    for (scanf("%d",&t);t;--t)
    {
        scanf("%d",&n);
        for (RI i=1;i<=n;++i) all[i]=bkt[i]=0;
        for (RI i=1;i<=n;++i)
        {
            scanf("%d",&c[i]); ++all[c[i]];
            son[i]=0; v[i].clear();
        }
        for (RI i=1;i<n;++i)
        {
            int x,y; scanf("%d%d",&x,&y);
            v[x].push_back(y);
            v[y].push_back(x);
        }
        IN=OUT=0;
        for (RI i=1;i<=n;++i) inc(OUT,pw2[all[i]]-1);
        DFS1(); DSU();
        // for (RI i=1;i<=n;++i)
        // printf("i = %d, in[i] = %d, out[i] = %d\n",i,in[i],out[i]);
        int ans=0; DFS2(ans);
        printf("%d\n",ans);
    }
    return 0;
}

F. 方格填数

注意到题目要求最小,因此让每一段能断则断一定是最优的

一个关键的观察是:对于 \(r_i-l_i+1\ge 3\) 的位置,一定可以做为一个单独的隔断点,因为它们至少能和左右两边保持不同

因此这题实际上只要考虑取值唯一和两种取值的情况,做一个简单的 DP 即可

这个 DP 是把端点以及选哪个数作为状态,二元组总贡献和上一段的长度作为存储的信息

虽然乍一看这个 DP 状态数可能会爆,但利用经典单调性剔除掉无用的二元组后,交上去就直接过了,非常的神奇

#include<cstdio>
#include<iostream>
#include<vector>
#include<algorithm>
#define int long long
#define RI register int
#define CI const int&
using namespace std;
typedef pair <int,int> pi;
const int N=1e6+5,INF=1e18;
int t,n,l[N],r[N]; vector <pi> f[N][2];
inline int solve(CI beg,CI end)
{
    if (beg>end) return 0;
    for (RI i=beg;i<=end;++i)
    f[i][0].clear(),f[i][1].clear();
    f[beg][0].push_back({1,1});
    if (l[beg]!=r[beg]) f[beg][1].push_back({1,1});
    for (RI i=beg+1;i<=end;++i)
    {
        auto upt=[&](CI idx,vector <pi>& vec,CI num)
        {
            vector <pi> tmp;
            for (RI j=0;j<=1;++j)
            {
                if (f[idx-1][j].empty()) continue;
                for (auto [val,len]:f[idx-1][j])
                {
                    if (num!=l[idx-1]+j)
                    {
                        tmp.push_back({val+1,1});
                    } else
                    {
                        tmp.push_back({val+2*len+1,len+1});
                    }
                }
            }
            sort(tmp.begin(),tmp.end());
            int lst_len=INF;
            for (auto [val,len]:tmp)
            {
                if (len>=lst_len) continue;
                vec.push_back({val,len});
                lst_len=len;
            }
        };
        upt(i,f[i][0],l[i]);
        if (l[i]!=r[i]) upt(i,f[i][1],r[i]);
    }
    int res=INF;
    for (RI j=0;j<=1;++j)
    {
        for (auto [val,len]:f[end][j])
        res=min(res,val);
    }
    return res;
}
signed main()
{
    for (scanf("%lld",&t);t;--t)
    {
        scanf("%lld",&n);
        for (RI i=1;i<=n;++i) scanf("%lld",&l[i]);
        for (RI i=1;i<=n;++i) scanf("%lld",&r[i]);
        int lst=0,ans=0;
        for (RI i=1;i<=n;++i)
        if (r[i]-l[i]+1>=3) ans+=solve(lst+1,i-1)+1,lst=i;
        ans+=solve(lst+1,n);
        printf("%lld\n",ans);
    }
    return 0;
}

K. 开火车

这题队友讨论出的,我题目都没看

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

const int N = 5e5+5;
int n, A[N], B[N], cnt[N], pir[N];

void solve() {
    cin >> n;
    for (int i=1; i<=n; ++i) cnt[i] = 0, B[i] = -1, pir[i]=-1;
    for (int i=1; i<=n; ++i) cin >> A[i], ++cnt[A[i]];
    vector<int> vec;
    for (int i=1; i<=n; ++i) {
        if (0==cnt[i]) vec.push_back(i);
    }

    for (int i=1; i<=n; ++i) {
        if (cnt[A[i]]==2 && A[i] != A[1]) {
            if (-1 == pir[A[i]]) {
                int x = vec.back(); vec.pop_back();
                pir[A[i]] = x;
                B[i-1] = x;
            } else {
                B[i-1] = pir[A[i]];
            }

        }
    }
    for (int i=n, j=n; i>0 && j>0; --i, --j) {
        while (i>0 && cnt[A[i]]>1) --i; 
        while (j>=i && B[j]!=-1) --j;
        if (i>0) B[j] = A[i];
    }
    // printf("B:"); for (int i=1; i<=n; ++i) printf("%d ", B[i]); puts("");
    int ans = 0;
    if (vec.size() > 0) {
        ans = 1;
        for (int i=1; i<=n; ++i) if (B[i]==-1) B[i] = vec[0];
    }
    cout << ans << '\n';
    for (int i=1; i<=n; ++i) cout << B[i] << (i==n ? '\n' : ' ');
}

signed main() {
    ios::sync_with_stdio(0); cin.tie(0);
    int T; cin >> T; while (T--) solve();
    return 0;
}

L. 列队

考虑把每次 PK 的两个人留下的人放到队列末尾,则最后得到一个长 \(2n-1\) 的序列

可以很容易地维护出修改 \(i\in [1,n]\) 的位置时,会对 \(j\in[n+1,2n-1]\) 的哪些位置产生影响,并且影响的位置数量是 \(O(\log n)\)

因此拿一个树状数组暴力维护修改即可,总复杂度 \(O(n\log^2 n)\)

#include<cstdio>
#include<iostream>
#include<vector>
#include<set>
#define RI register int
#define CI const int&
using namespace std;
const int N=200005;
int n,m,ls[N],rs[N],out[N],rem[N];
set <int> idx[N]; vector <int> pos[N];
class Tree_Array
{
    private:
        long long bit[N];
    public:
        #define lowbit(x) (x&-x)
        inline void add(RI x,CI y)
        {
            for (;x<n;x+=lowbit(x)) bit[x]+=y;
        }
        inline long long get(RI x,long long res=0)
        {
            for (;x;x-=lowbit(x)) res+=bit[x]; return res;
        }
        #undef lowbit
}BIT;
int main()
{
    scanf("%d%d",&n,&m);
    for (RI i=1;i<=n;++i)
    {
        scanf("%d",&rem[i]);
        idx[i].insert(i);
    }
    auto upt=[&](CI x)
    {
        out[x]=max(rem[ls[x]],rem[rs[x]]);
        rem[x]=min(rem[ls[x]],rem[rs[x]]);
    };
    for (RI i=n+1,j=1;i<2*n;++i)
    {
        auto merge=[&](set <int> A,set <int> B)
        {
            if ((int)A.size()<(int)B.size()) swap(A,B);
            for (auto x:B) A.insert(x);
            return A;
        };
        idx[i]=merge(idx[j],idx[j+1]);
        ls[i]=j; rs[i]=j+1; upt(i);
        j+=2;
    }
    for (RI i=1;i<n;++i)
    {
        for (auto x:idx[n+i]) pos[x].push_back(i);
        BIT.add(i,out[n+i]);
    }
    while (m--)
    {
        char opt[5]; int x,y;
        scanf("%s%d%d",opt,&x,&y);
        if (opt[0]=='C')
        {
            swap(rem[x],rem[y]);
            for (auto p:pos[x])
            {
                BIT.add(p,-out[n+p]);
                upt(n+p);
                BIT.add(p,out[n+p]);
            }
            for (auto p:pos[y])
            {
                BIT.add(p,-out[n+p]);
                upt(n+p);
                BIT.add(p,out[n+p]);
            }
        } else printf("%lld\n",BIT.get(y)-BIT.get(x-1));
    }
    return 0;
}

Postscript

这周末还有一场 CCPC 重庆要现场打,虽然按道理说去年已经拿到了 CCPC 的 Au,今年也有幸参加了 CCPC Final

但不管怎么说还是尽量往好的成绩努力吧,今年的两场 ICPC Regional 队友都实力带飞我拿了 Au,下场牢闪能不能发发力啊

posted @ 2025-11-27 15:10  空気力学の詩  阅读(91)  评论(0)    收藏  举报