The 2025 ICPC Asia Wuhan Regional Contest

Preface

上周的武汉站,VP 之前就听说这场题偏向性严重(前中期大量 CF 思博题和神秘构造),给人的体验不太好

打的时候感觉确实如此,很多题需要对上脑电波;我个人开场被 E 签到单防快 1h,然后扔给队友就秒切;转头看队友不会的 M 随便画了下就会了

整场的题给人的都是这样一种感觉,不过好在我们队三个人都能找到自己舒适区里的题,最后堪堪 7 题,本来 A 题因为有场外因素以及基本套到类欧的式子上去了,结果不会处理负数直接倒闭


A. 种树

这题的本质就是问下面这个式子:

\[Ans=\sum_{t=0}^{n-1} [(ft+x)\bmod m<(gt+y)\bmod m] \]

对于 \(a,b\in[0,m-1]\)\([a<b]\leftrightarrow1+\lfloor\frac{b-a-1}{m}\rfloor\),代入原来的式子,把取模用下取整代换后整理得:

\[Ans=n + \sum_{t=0}^{n-1} \lfloor\frac{ft+x}{m}\rfloor - \sum_{t=0}^{n-1} \lfloor\frac{gt+y}{m}\rfloor + \sum_{t=0}^{n-1} \lfloor\frac{(g-f)t+y-x-1}{m}\rfloor \]

众所周知类欧可以在 \(O(\log n)\) 的时间内求解形如 \(\sum_{i=0}^n \lfloor\frac{ai+b}{c}\rfloor\) 的式子,直接抄板子即可

注意这题中 \(a,b\) 可能为负数,因此需要对原来的板子进行一些魔改,赛后徐神鼓捣了一会才通过

#include<cstdio>
#include<iostream>
#define int long long
#define RI register int
#define CI const int&
using namespace std;
int t,f,x,g,y,n,m;
inline int F(int a,int b,int c,int n)
{
    if (c<0) a=-a,b=-b,c=-c;
	if (a==0) return ({
		int bc=b/c;
		if(b<0&&b%c!=0) bc-=1;
		(n+1)*bc;
	});
    int m=(a*n+b)/c;
    if (a>=c||a<0||b>=c||b<0) return ({
        int ac=(a%c+c)%c;
        int bc=(b%c+c)%c;
        (a-ac)/c*n*(n+1)/2+(b-bc)/c*(n+1)+F(ac,bc,c,n);
    });
    else return n*m-F(c,c-b-1,a,m-1);
}
signed main()
{
    for (scanf("%lld",&t);t;--t)
    {
        scanf("%lld%lld%lld%lld%lld%lld",&f,&x,&g,&y,&n,&m);
        printf("%lld\n",n+F(f,x,m,n-1)-F(g,y,m,n-1)+F(g-f,y-x-1,m,n-1));
    }
    return 0;
}

B. 77G网络

套路模板题,只要你知道这题相关的知识点并且有时间就非常 trivial 的一个题

首先这题一眼 2-SAT 模型,每条树边的限制很好描述,考虑额外的 \(m\) 条限制

每条限制会让点 \(x\) 和其子树中某个深度的点集 \(y\) 产生新的约束,直接暴力建边复杂度肯定会爆,因此一眼考虑线段树优化建图

要让点集 \(y\) 中的点编号连续,由于和深度有关很容易想到用 BFS 序编号,而找到对应的编号区间可以二分,具体地:

求出每个点的 DFS 序,在某个深度的层中,满足这层的点 BFS 序连续,且 DFS 序单调增;用 DFS 序判断在子树内的性质二分找到两个端点即可

总复杂度 \(O((n+m)\log n)\)

#include<cstdio>
#include<iostream>
#include<vector>
#include<queue>
#define RI register int
#define CI const int&
using namespace std;
const int N=400005,NN=N*20;
int t,n,m,anc[N],bfn[N],rbfn[N],dep[N],L[N],R[N],idx,tot;
vector <int> v[N],layer[N],E[NN];
int dfn[NN],low[NN],stk[NN],top,instk[NN],bel[NN],scc;
inline void DFS(CI now=1)
{
    L[now]=++idx;
    for (auto to:v[now]) DFS(to);
    R[now]=idx;
}
inline int ID0(CI x)
{
    return x;
}
inline int ID1(CI x)
{
    return n+x;
}
class Segment_Tree
{
    private:
        int up[N<<2],dn[N<<2];
    public:
        #define TN CI now=1,CI l=1,CI r=n
        #define LS now<<1,l,mid
        #define RS now<<1|1,mid+1,r
        inline void build(TN)
        {
            up[now]=++tot; dn[now]=++tot;
            if (l==r)
            {
                E[ID1(rbfn[l])].push_back(up[now]);
                E[dn[now]].push_back(ID0(rbfn[l]));
                return;
            }
            int mid=l+r>>1; build(LS); build(RS);
            E[up[now<<1]].push_back(up[now]);
            E[up[now<<1|1]].push_back(up[now]);
            E[dn[now]].push_back(dn[now<<1]);
            E[dn[now]].push_back(dn[now<<1|1]);
        }
        inline void link(CI x,CI beg,CI end,TN)
        {
            if (beg<=l&&r<=end)
            {
                E[ID1(x)].push_back(dn[now]);
                E[up[now]].push_back(ID0(x));
                return;
            }
            int mid=l+r>>1;
            if (beg<=mid) link(x,beg,end,LS);
            if (end>mid) link(x,beg,end,RS);
        }
        #undef TN
        #undef LS
        #undef RS
}SEG;
inline void Tarjan(CI now)
{
    dfn[now]=low[now]=++idx;
    stk[++top]=now; instk[now]=1;
    for (auto to:E[now])
    if (!dfn[to]) Tarjan(to),low[now]=min(low[now],low[to]);
    else if (instk[to]) low[now]=min(low[now],dfn[to]);
    if (dfn[now]==low[now])
    {
        bel[now]=++scc; instk[now]=0;
        while (stk[top]!=now)
        bel[stk[top]]=scc,instk[stk[top--]]=0;
        --top;
    }
}
int main()
{
    for (scanf("%d",&t);t;--t)
    {
        scanf("%d",&n); idx=0;
        for (RI i=2;i<=n;++i)
        {
            scanf("%d",&anc[i]);
            v[anc[i]].push_back(i);
            E[ID0(i)].push_back(ID1(anc[i]));
            E[ID0(anc[i])].push_back(ID1(i));
        }
        queue <int> q; q.push(1); dep[1]=1;
        while (!q.empty())
        {
            int now=q.front(); q.pop();
            rbfn[bfn[now]=++idx]=now;
            layer[dep[now]].push_back(now);
            for (auto to:v[now]) dep[to]=dep[now]+1,q.push(to);
        }
        idx=0; DFS();
        tot=2*n; SEG.build();
        for (scanf("%d",&m);m;--m)
        {
            int x,y; scanf("%d%d",&x,&y);
            if (dep[x]+y>n||layer[dep[x]+y].empty()) continue;
            int l,r,lb=-1,rb=-1;
            l=0; r=(int)layer[dep[x]+y].size()-1;
            while (l<=r)
            {
                int mid=l+r>>1;
                if (L[layer[dep[x]+y][mid]]>=L[x]) lb=mid,r=mid-1; else l=mid+1;
            }
            l=0; r=(int)layer[dep[x]+y].size()-1;
            while (l<=r)
            {
                int mid=l+r>>1;
                if (L[layer[dep[x]+y][mid]]<=R[x]) rb=mid,l=mid+1; else r=mid-1;
            }
            if (lb==-1||rb==-1) continue;
            // printf("%d: ",x);
            // for (RI i=lb;i<=rb;++i)
            // printf("%d (%d) ",layer[dep[x]+y][i],bfn[layer[dep[x]+y][i]]);
            // putchar('\n');
            SEG.link(x,bfn[layer[dep[x]+y][lb]],bfn[layer[dep[x]+y][rb]]);
        }
        idx=scc=0;
        for (RI i=1;i<=tot;++i)
        if (!dfn[i]) Tarjan(i);
        bool flag=1;
        for (RI i=1;i<=n;++i)
        if (bel[ID0(i)]==bel[ID1(i)]) { flag=0; break; }
        if (!flag) puts("No"); else
        {
            vector <int> ans;
            for (RI i=1;i<=n;++i)
            if (bel[ID1(i)]<bel[ID0(i)]) ans.push_back(i);
            puts("Yes");
            printf("%d\n",(int)ans.size());
            for (auto x:ans) printf("%d ",x);
            putchar('\n');
        }
        for (RI i=1;i<=n;++i) v[i].clear(),layer[i].clear();
        for (RI i=1;i<=tot;++i) E[i].clear(),dfn[i]=0;
    }
    return 0;
}

C. 不对称填数

神秘构造,难点在于对于各种 Corner Case 的讨论

对于 \(n,m\) 都较大的情况,由于其中必有一边是 \(3\) 的倍数,不妨钦定 \(3\mid n\),则可以由以下 \(3\times 6\) 的子结构重复拼凑出一组解:

const int ch[3][6] = {
    {0, 0, 1, 1, 2, 2}, 
    {2, 1, 0, 2, 1, 0}, 
    {1, 2, 2, 0, 0, 1}
};

(往下扩展时每次要把该结构对称变换一下,这里具体实现看代码)

然而这种情况对于 \(m=4\) 的 Case 会构造出 \(2\) 不连通的方案,因此对于这种情形要单独 fix

一种合法的方式是构造一个 \(6\times 4\) 的合法子结构,每次向下扩展不需要对称直接复制即可:

const int ch2[6][4] = {
    {2, 1, 0, 1},
    {2, 0, 1, 0},
    {0, 2, 2, 1},
    {1, 0, 1, 2},
    {0, 1, 0, 2},
    {1, 2, 2, 0},
};

最后还要对 \(\min(n,m)\le 2\) 的情况做特判,然后注意下分讨的时候别漏情况就行

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

const int N = 1e3+5;
int n, m, trans, f[N][N];
const int ch[3][6] = {
    {0, 0, 1, 1, 2, 2}, 
    {2, 1, 0, 2, 1, 0}, 
    {1, 2, 2, 0, 0, 1}
};
const int ch2[6][4] = {
    {2, 1, 0, 1},
    {2, 0, 1, 0},
    {0, 2, 2, 1},
    {1, 0, 1, 2},
    {0, 1, 0, 2},
    {1, 2, 2, 0},
};

void printans() {
    if (trans) swap(n, m);
    for (int i=0; i<n; ++i) {
        for (int j=0; j<m; ++j) cout << (trans ? f[j][i] : f[i][j]);
        cout << '\n';
    }
}

bool solve() {
    trans = 0;
    cin >> n >> m;
    if (n%3!=0 || 3==m) {
        swap(n, m); trans=1;
    }

    if (1==m) {
        if (n>6) return false;
        if (3==n) for (int i=0; i<3; ++i) f[i][0]=i;
        if (6==n) for (int i=0; i<6; ++i) f[i][0]=i/2;
    } else if (2==m) {
        if (n>6) return false;
        if (3==n) for (int i=0; i<3; ++i) f[i][0]=f[i][1] = i;
        if (6==n) {
            f[0][0] = f[0][1] = f[1][0] = f[2][1] = 0;
            f[1][1] = f[2][0] = f[3][1] = f[4][0] = 1;
            f[3][0] = f[4][1] = f[5][0] = f[5][1] = 2;
        }
    } else if (4==m) {
        for (int i=0; i<n; ++i) {
            int r = i%6;
            // if (r>=3) r = 5-r;
            for (int j=0; j<4; ++j) f[i][j] = ch2[r][j];
        }
    } else {
        for (int i=0; i<n; ++i) for (int j=0; j<=m; ++j) {
            int r = i%6;
            if (r>=3) r = 5-r;
            f[i][j] = ch[r][j%6];
        }
    }
    return true;
}

signed main() {
    ios::sync_with_stdio(0); cin.tie(0);
    int T; cin >> T; while (T--) {
        if (!solve()) cout << "No\n";
        else {
            cout << "Yes\n";
            printans();
        }
    }
    return 0;
}

E. 变魔术

谨以此题纪念某人面对签到思考了 1h 得到三种假做法的战犯行为

首先特判两个序列初始时就相等的情况,然后考虑能够进行变换的必要条件是两个序列中都要有至少一个相邻的 pair

考虑一对相邻的 pair 可以一直移动,并将序列中其它数的值一同带着修改

手玩一下会发现此时操作的不变量是序列中奇数的个数,根据这个来判断即可

#include<cstdio>
#include<iostream>
#include<algorithm>
#include<set>
#define RI register int
#define CI const int&
using namespace std;
const int N=100005;
int t,n,a[N],b[N];
int main()
{
    for (scanf("%d",&t);t;--t)
    {
        scanf("%d",&n);
        for (RI i=1;i<=n;++i)
        scanf("%d",&a[i]);
        for (RI i=1;i<=n;++i)
        scanf("%d",&b[i]);
        sort(a+1,a+n+1);
        sort(b+1,b+n+1);
        bool all_same=1;
        for (RI i=1;i<=n;++i)
        {
            if (a[i]!=b[i]) all_same=0;
        }
        if (all_same) { puts("Yes"); continue; }
        set <int> exta,extb;
        for (RI i=1;i<=n;++i)
        {
            exta.insert(a[i]);
            extb.insert(b[i]);
        }
        bool a_has_pair=0,b_has_pair=0;
        for (RI i=1;i<=n;++i)
        {
            if (exta.count(a[i]+1)) a_has_pair=1;
            if (extb.count(b[i]+1)) b_has_pair=1;
        }
        if (!a_has_pair||!b_has_pair) { puts("No"); continue; }
        int odd_a=0,odd_b=0;
        for (RI i=1;i<=n;++i)
        {
            if (a[i]%2==1) ++odd_a;
            if (b[i]%2==1) ++odd_b;
        }
        puts(odd_a==odd_b?"Yes":"No");
    }
    return 0;
}

F. 分治

不难发现这题分治的壳子是假的,本质是要求最少的点 cover 所有区间

令对应的点数为 \(cnt\),则答案为 \(\lceil\log_2(cnt+1)\rceil\),而求解 \(cnt\) 是个老生常谈的贪心

#include<cstdio>
#include<iostream>
#include<algorithm>
#define RI register int
#define CI const int&
#define fi first
#define se second
using namespace std;
const int N=1e6+5;
int t,n,q; pair <int,int> itv[N];
int main()
{
    for (scanf("%d",&t);t;--t)
    {
        scanf("%d%d",&n,&q);
        for (RI i=1;i<=q;++i)
        scanf("%d%d",&itv[i].fi,&itv[i].se);
        sort(itv+1,itv+q+1);
        int cover=0,L=1,R=n;
        for (RI i=1;i<=q;++i)
        {
            L=max(L,itv[i].fi); R=min(R,itv[i].se);
            if (L>R) ++cover,L=itv[i].fi,R=itv[i].se;
        }
        ++cover; int ans=1;
        while ((1<<ans)-1<cover) ++ans;
        printf("%d\n",ans);
    }
    return 0;
}

H. 还原数组

把问题想象在一棵 01-Trie 上,我们每次询问一个根到当前节点代表的数(或者其取反的数),可以确定其子树中最小/最大的数

初始时可以先用两次询问确定整个序列的最小/最大值,然后递归处理即可;中间的 \(n-2\) 个数需要两次询问,总次数 \(2(n-2)+2=2n-2\)

#include <bits/stdc++.h>

// #define DEBUG

constexpr int HKR = 30, ARS = (1 << HKR) - 1;
std::vector<int> a;

int query_raw(int r) {
#ifdef DEBUG
    int res = 0;
    for(auto a: a) res = std::max(res, a ^ r);
    // std::cerr << "QUERY " << r << ", RESPONSE " << (res ^ r) << char(10);
    return res ^ r;
#else
    std::cout << "? " << r << std::endl;
    int res; std::cin >> res;
    return res ^ r;
#endif
}

int query_max(int mask, int mask_len) {
    return query_raw(~mask & ARS & ~((1 << (HKR - mask_len)) - 1));
}

int query_min(int mask, int mask_len) {
    return query_raw((~mask | ((1 << (HKR - mask_len)) - 1)) & ARS);
}

int n;
std::set<int> answer;

void output() {
    std::cout << "!";
    for(auto ans: answer) std::cout << " " << ans;
    std::cout << std::endl;
}

void dfs(int min, int max) {
    // std::cerr << "dfs(" << min << ", " << max << ")\n";
    int mask_len = __builtin_clz(min ^ max) - (32 - HKR) + 1; // -2 because 32 - HKR = 2
    if(mask_len == HKR) return ;
    if(answer.size() == n) return ;
    int nmax = query_max(min, mask_len);
    if(nmax != min) {
        assert(nmax != max);
        answer.insert(nmax);
        dfs(min, nmax);
    }
    if(answer.size() == n) return ;
    int nmin = query_min(max, mask_len);
    if(nmin != max) {
        assert(nmin != min);
        answer.insert(nmin);
        dfs(nmin, max);
    }
    return ;
}

void work() {
    std::cin >> n;
#ifdef DEBUG
    a.resize(n);
    for(auto &a: a) std::cin >> a;
#endif
    answer.clear();
    int min = query_min(0, 0);
    int max = query_max(0, 0);
    answer.insert(min);
    answer.insert(max);
    dfs(min, max);
    output();
    return ;
}

int main() {
    std::ios::sync_with_stdio(false);

    int T; std::cin >> T;
    while(T--) work();
    return 0;
}

K. 整理书架

很 CF 风格的一个题

由于 \(a_i\)\(b_i\) 可以任意交换,因此一个数在哪个数组中是不重要的,重要的只是它的下标

有解的必要条件是所有数的出现次数为偶数,而对于某个数 \(x\)

我们将其出现的所有下标拎出来之后,显然将相邻的两两匹配总代价最小

根据经典 trick,交换的代价等价于直接移动的代价之和再除以二,因此我们不考虑交换只考虑直接移动构造答案

从前往后构造整个序列,如果 \(a_i=b_i\) 则忽略,否则看 \(a_i\) 下次出现的位置和 \(b_i\) 下次出现的位置谁更靠前就把谁换过来即可

#include<cstdio>
#include<iostream>
#include<vector>
#include<algorithm>
#define RI register int
#define CI const int&
using namespace std;
const int N=200005;
int t,n,a[N],b[N]; vector <int> pos[N];
int main()
{
    for (scanf("%d",&t);t;--t)
    {
        scanf("%d",&n);
        for (RI i=1;i<=n;++i) pos[i].clear();
        for (RI i=1;i<=n;++i)
        scanf("%d",&a[i]),pos[a[i]].push_back(i);
        for (RI i=1;i<=n;++i)
        scanf("%d",&b[i]),pos[b[i]].push_back(i);
        bool flag=1;
        for (RI i=1;i<=n;++i)
        {
            if ((int)pos[i].size()%2==1) { flag=0; break; }
            sort(pos[i].begin(),pos[i].end(),greater <int>());
        }
        if (!flag) { puts("-1"); continue; }
        long long cost=0;
        vector <pair <int,int>> seq;
        for (RI i=1;i<=n;++i)
        {
            if (a[i]==b[i])
            {
                pos[a[i]].pop_back();
                pos[b[i]].pop_back();
                continue;
            }
            auto SWAP=[&](CI x,CI y)
            {
                seq.push_back({x,y});
                swap(a[x],b[y]);
                return;
            };
            int pa=pos[a[i]][(int)pos[a[i]].size()-2];
            int pb=pos[b[i]][(int)pos[b[i]].size()-2];
            // printf("pa = %d, pb = %d\n",pa,pb);
            if (pa<=pb)
            {
                if (b[pa]==a[i]) SWAP(i,i),SWAP(i,pa);
                else SWAP(i,i),SWAP(pa,pa),SWAP(i,pa);
                pos[a[i]].pop_back(); pos[a[i]].pop_back();
                cost+=pa-i;
            } else
            {
                if (b[pb]==b[i]) SWAP(i,pb);
                else SWAP(pb,pb),SWAP(i,pb);
                pos[b[i]].pop_back(); pos[b[i]].pop_back();
                cost+=pb-i;
            }
        }
        printf("%lld %d\n",cost,(int)seq.size());
        for (auto [x,y]:seq) printf("%d %d\n",x,y);
    }
    return 0;
}

M. 构造王国

手玩 \(n=2\) 的情况发现一种合法的构造为

\[\begin{matrix} a_1+1 & a_1\\ a_2 & a_2+1 \end{matrix} \]

考虑 \(n=3\) 的情况时自然想到构造 \(A_{i,j}=a_i+[i==j]\) 了,然后手动检验下发现确实是合法的

证明正确性的话可以用归纳法,但有一说一感觉这题没什么意思,有点先射箭再画靶的含义

#include<cstdio>
#include<iostream>
#define RI register int
#define CI const int&
using namespace std;
const int N=20,mod=998244353;
int n,a[N];
int main()
{
    scanf("%d",&n);
    for (RI i=1;i<=n;++i) scanf("%d",&a[i]);
    for (RI i=1;i<=n;++i)
    for (RI j=1;j<=n;++j)
    printf("%d%c",(a[i]+(i==j))%mod," \n"[j==n]);
    return 0;
}

Postscript

这周末就是这赛季第一场 Regional 了,希望别整个大活给自己操办仪式

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