2025“钉耙编程”中国大学生算法设计暑期联赛(2)02/06/08/09/12

个人做题顺序/大致按照难度排序

1. 1002 数上的图

知识点:二进制拆分,二进制相关函数

如果这两个数相等,则零次转化

如果这两个数的 \(count\)\(lowbit\) 相等,则可以一次转化过去

否则,剩下的任何情况,一定可以从两种方式之一转换过去:

  1. \(x\) 先转换为 \(count(y)\) ,再转换为 \(y\)
  2. \(x\) 先转换为 \(lowbit(y)\) ,再转换为 \(y\)

其中 \(lowbit\) 函数需要手写,固定写法。

\(count\) 函数:__builtin_popcountll (对应long long)或 __builtin_popcount (对应int) 本题需要用 ll

#include<bits/stdc++.h>
#define int long long
using namespace std;
using pii=pair<int,int>;
using ll = long long;
using ull = unsigned long long;
using i128 = __int128;
const ll inf = 1e18;
const int mod = 998244353;

int lowbit(int x){
    return x&-x;
}

void solve(){
    int n,x,y;
    cin>>n>>x>>y;

    if(x==y){
        cout<<0<<endl;
        return;
    }
    if(__builtin_popcountll(x)==__builtin_popcountll(y) || lowbit(x)==lowbit(y)){
        cout<<1<<endl;
    }
    else{
        cout<<2<<endl;
    }
}

signed main(){
    ios::sync_with_stdio(0);
    cin.tie(0);

    int ct=1;
    cin>>ct;
    while(ct--){
        solve();
    }
    return 0;
}

2. 1008 井

知识点:数学期望,概率

首先,需要找到任一被选中的格子,最优的方式是沿着对角线一个一个找。这一步最少找 \(1\) 次,最多找 \(n\) 次,期望是 \((1+n) / 2\)

找到任一格子后,还需要确定是行还是列,最优方式是翻开这一格子上面的和左面的,期望是 \((1+2)/2\)

剩下 \(n-2\) 个格子已经确定,期望是 \((n-2)\)

#include<bits/stdc++.h>
#define int long long
using namespace std;
using pii=pair<int,int>;
using ll = long long;
using ull = unsigned long long;
//using i128 = __int128_t;
const ll inf = 1e18;
const int mod = 998244353;

void solve(){
    double n;
    cin>>n;
    double ans=(1+n)/2+1.5+n-2;
    printf("%.4lf\n",ans);
}

signed main(){

    int ct=1;
    cin>>ct;
    while(ct--){
        solve();
    }
    return 0;
}

3. 1006半

知识点:树状数组

假设第 \(i\) 个人的排名分别是 \(a_i,b_i\)

则答案为:\(a_i\) 前面的人数 + \(b_i\) 前面的人数 - 两部分共有的人数

我们从前往后枚举 \(a\) 数组,并将 \(a[1] ~到~ a[i-1]\) 所有人在 \(b\) 数组中的排名放进树状数组。

枚举到 \(a[i]\) 时查询树状数组中有多少个位于 \(1 ~到~ rkb[i]\) 的元素即可

#include<bits/stdc++.h>
using namespace std;
using pii=pair<int,int>;
using ll = long long;
using ull = unsigned long long;
//using i128 = __int128_t;
const ll inf = 1e18;
const int mod = 998244353;

struct TreeArray {
    int n;
    vector<ll> tr;

    TreeArray(int _n) { init(_n); }
    TreeArray() {}

    // 初始化,开辟大小为 n+1 的数组,下标从 1 开始使用
    void init(int _n) {
        n = _n;
        tr.assign(n + 1, 0);
    }

    // lowbit 函数,获取最低位的 1
    inline int lowbit(int x) {
        return x & -x;
    }

    // 在下标 x 增加值 c
    void insert(int x, ll c) {
        for (int i = x; i <= n; i += lowbit(i)) {
            tr[i] += c;
        }
    }

    // 获取前缀和 [1..x]
    ll sum(int x) {
        ll ans = 0;
        for (int i = x; i > 0; i -= lowbit(i)) {
            ans += tr[i];
        }
        return ans;
    }

    ll query(int l,int r){
        return sum(r)-sum(l-1);
    }

    // 二分查找:返回最大的 x,使得 sum(x) <= k
    // 如果所有前缀和都大于 k,则返回 0
    int select(ll k) const {
        int x = 0;
        ll cur = 0;
        // i 从最高 2^⌊lg n⌋ 开始
        for (int i = 1 << (31 - __builtin_clz(n)); i > 0; i >>= 1) {
            if (x + i <= n && cur + tr[x + i] <= k) {
                x += i;
                cur += tr[x];
            }
        }
        return x;
    }
};

void solve(){
    int n;
    cin>>n;
    vector<int> a(n+1),b(n+1);
    vector<int> cnt(n+1);
    vector<int> pa(n+1),pb(n+1);
    vector<int> ans(n+1);
    int now=0;

    TreeArray tr(n);

    for(int i=1;i<=n;i++){
        cin>>a[i];
        pa[a[i]]=i;
    }

    for(int i=1;i<=n;i++){
        cin>>b[i];
        pb[b[i]]=i;
    }

    for(int i=1;i<=n;i++){
        ans[i]=pa[i]-1+pb[i]-1;
    }

    for(int ra=1;ra<=n;ra++){
        int id=a[ra];
        int rb=pb[id];

        ans[id]-=tr.query(1,rb);
        tr.insert(rb,1);
    }

    for(int i=1;i<=n;i++){
        cout<<ans[i]<<" ";
    }
    cout<<endl;

}

signed main(){
    ios::sync_with_stdio(0);
    cin.tie(0);

    int ct=1;
    cin>>ct;
    while(ct--){
        solve();
    }
    return 0;
}

4. 1009 苹果树

知识点:树链剖分,线段树

做本题需要掌握树链剖分的性质。

对于查询操作,显然要使用树链剖分

对于修改操作,如果暴力修改,复杂度显然不可接受

考虑对点 \(u\) 修改时,会被影响的点可以分成三类:

  1. \(fa[u]\)
  2. \(son[u]\)\(u\) 的重子节点)
  3. \(u\) 的轻子节点

对于1,2, 在每次修改是可以 O(1) 的直接修改,而对于轻子节点,根据重链剖分的性质,每个轻子节点都是一条重链的头节点。

而熟练剖分在查询时,最多会经过 log(n) 条重链。所以对于这些轻子节点,我们可以在查询经过重链头节点时,顺便计算出来。

同时因为点权只增不减,所以可以无脑取 MAX。

注意查询操作计算重链头节点时不能修改原值,因为会和其他修改/查询操作冲突。

#include<bits/stdc++.h>
#define int long long
using namespace std;
using pii=pair<int,int>;
using ll = long long;
using ull = unsigned long long;
//using i128 = __int128_t;
const ll inf = 1e18;
const int mod = 998244353;

class SegmentTree{
    public:
    #define lc u<<1
    #define rc u<<1|1

    struct Node{
        int l,r,mx;
        int add;
    };

    int n;
    vector<int> w;
    vector<Node> tr;

    SegmentTree(int n){
        init(n);
    }

    void init(int n){
        this->n=n;
        w.resize(n+10);
        tr.resize(4*n+10);
    }

    void pushup(int u){
        tr[u].mx=max(tr[rc].mx,tr[lc].mx);
    }

    void pushdown(int u){
        if(tr[u].add){
            tr[rc].add+=tr[u].add;
            tr[lc].add+=tr[u].add;
            tr[rc].mx+=tr[u].add;
            tr[lc].mx+=tr[u].add;
            tr[u].add=0;
        }
        return;
    }

    void build(int u,int l,int r){
        if(l==r) tr[u]={l,r,w[r],0};
        else{
            tr[u]={l,r};
            int mid=l+r>>1;
            build(lc,l,mid);
            build(rc,mid+1,r);
            pushup(u);
        }
    }

    int query(int u,int l,int r){
        if(l<=tr[u].l && r>=tr[u].r) return tr[u].mx;
        else{
            pushdown(u);
            int mx=0,mid=tr[u].l+tr[u].r>>1;
            if(l<=mid) mx=query(lc,l,r);
            if(r>mid) mx=max(mx,query(rc,l,r));
            return mx;
        }
    }

    void modify(int u,int l,int r,int k){
        if(l<=tr[u].l && r>=tr[u].r){
            tr[u].mx+=k;
            tr[u].add+=k;
        }
        else{
            pushdown(u);
            int mid=tr[u].r+tr[u].l>>1;
            if(l<=mid) modify(lc,l,r,k);
            if(r>mid) modify(rc,l,r,k);
            pushup(u);
        }
    }
};

struct HLD {
    int n, root;
    vector<vector<int>> adj;  // 邻接表
    vector<int> sz, dep, fa, son;      // 子树大小、深度、父节点、重儿子
    vector<int> top, id, rev, seq, w;  // 链顶、DFN编号、逆向映射、权值线性化数组、原权值
    int timer;
    SegmentTree seg;
    vector<int> tag;

    // 构造函数,传入节点数
    HLD(int _n = 0) : n(_n), seg(_n) {
        adj.assign(n+1, {});
        sz.assign(n+1, 0);
        dep.assign(n+1, 0);
        fa.assign(n+1, 0);
        son.assign(n+1, 0);
        top.assign(n+1, 0);
        id.assign(n+1, 0);
        rev.assign(n+1, 0);
        seq.assign(n+1, 0);
        w.assign(n+1, 0);
        tag.assign(n+1,0);
        timer = 0;
    }

    // 添加一条无向边 u-v
    void addEdge(int u, int v){
        adj[u].push_back(v);
        adj[v].push_back(u);
    }
    // 设置节点 u 的初始权值
    void setWeight(int u, int v){ w[u] = v; }

    /**
     * dfs1:计算每个节点的子树大小和重儿子
     * u:当前节点,p:父节点
     */
    void dfs1(int u, int p){
        fa[u] = p;
        dep[u] = dep[p] + 1;
        sz[u] = 1;
        son[u] = 0;
        for (int v : adj[u]) if (v != p) {
            dfs1(v, u);
            sz[u] += sz[v];
            // 选出最大的子树作为重儿子
            if (!son[u] || sz[v] > sz[son[u]]) son[u] = v;
        }
    }

    /**
     * dfs2:沿重链深入,分配 DF
     * u:当前节点,t:当前链顶节点
     */
    void dfs2(int u, int t){
        top[u] = t;
        id[u] = ++timer;
        rev[timer] = u;
        seq[timer] = w[u];
        // 先遍历重儿子,保持链上连续
        if (son[u]) dfs2(son[u], t);
        // 再遍历其他轻儿子,各自开新链
        for (int v : adj[u]) if (v != fa[u] && v != son[u]) {
            dfs2(v, v);
        }
    }

    /**
     * build:完成树链剖分并建线段树
     * _root:指定根节点编号
     */
    void build(int _root){
        root = _root;
        dep[0] = 0;
        dfs1(root, 0);
        dfs2(root, root);
        // 将线性化后的权值拷贝给线段树
        for (int i = 1; i <= n; i++) seg.w[i] = seq[i];
        seg.build(1, 1, n);
    }

    /**
     * modifyPath:对路径 u-v 上的所有节点加上 k
     */
    void modifyPath(int u, int v, int k){
        while (top[u] != top[v]){
            if (dep[top[u]] < dep[top[v]]) swap(u, v);
            seg.modify(1, id[top[u]], id[u], k);
            u = fa[top[u]];
        }
        if (dep[u] < dep[v]) swap(u, v);
        seg.modify(1, id[v], id[u], k);
    }

    /**
     * queryPath:查询路径 u-v 上所有节点的权值和
     */
        int queryPath(int u, int v) {
        int res = 0;

        while (top[u] != top[v]) {
            if (dep[top[u]] < dep[top[v]]) swap(u, v);
            res = max(res, seg.query(1, id[top[u]], id[u]));
            if (fa[top[u]]) { // fa[root] is 0
                res = max(res, seg.query(1, id[top[u]], id[top[u]]) + tag[fa[top[u]]]);
            }
            u = fa[top[u]];
        }
        // 此刻 u 和 v 在同一条链上
        if (dep[u] < dep[v]) swap(u, v);
        // 3) 查询最后一条链上的区间最大值
        res = max(res, seg.query(1, id[v], id[u]));
        
        //!!!!!!!!!!! 单独修正 LCA 节点(v)的值,因为它可能是一个未被修正过的链顶
        if (top[v] == v && fa[v] != 0) {
            res = max(res, seg.query(1, id[v], id[v]) + tag[fa[v]]);
        }

        return res;
    }
};

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

    HLD tr(n);
    int b=sqrt(n)+1;

    for(int i=1;i<=n;i++){
        int x;
        cin>>x;
        tr.setWeight(i,x);
    }

    for(int i=1;i<n;i++){
        int u,v;
        cin>>u>>v;
        tr.addEdge(u,v);
    }

    tr.build(1);

    while(m--){
        int op;
        cin>>op;

        if(op==1){
            int x,y;
            cin>>x>>y;

            cout<<tr.queryPath(x,y)<<endl;
        }
        else{
            int x,z;
            cin>>x>>z;
            if(tr.fa[x]) tr.modifyPath(tr.fa[x],tr.fa[x],z);
            if(tr.son[x]) tr.modifyPath(tr.son[x],tr.son[x],z);
            tr.tag[x]+=z;
        }
    }


}

signed main(){
    ios::sync_with_stdio(0);
    cin.tie(0);

    int ct=1;
    cin>>ct;
    while(ct--){
        solve();
    }
    return 0;
}

5. 1012 子集

知识点:线性基,高斯消元

题目要求取值不能相邻,所以取值的间隔要么是1,要么是2,间隔没有必要是更大值。

假设有五个数,如果选第1个数,且下一个数是第5个数(此时间隔是3),把这两个数插入线性基

此时则可以再把第3个数插入线性基,结果一定不劣

所以 \(DFS\) 枚举所有可能情况即可,中间需要动态插入线性基

#include<bits/stdc++.h>
#define int long long
using namespace std;
using pii=pair<int,int>;
using ll = long long;
using ull = unsigned long long;
//using i128 = __int128_t;
const ll inf = 1e18;
const int mod = 998244353;

class XorBasis {//基于高斯消元,逐个插入值
public:
    const int BITS = 62;
    vector<long long> basis; // 存储线性基的基底
    int k;                   // 基底中向量的个数
    bool has_zero;           // 标记原数集中是否能异或出 0

    XorBasis() : basis(BITS + 1, 0), k(0), has_zero(false) {}

    void insert(long long num) {
        for (int i = BITS; i >= 0; i--) {
            if (!((num >> i) & 1)) {
                continue;
            }
            if (!basis[i]) {
                basis[i] = num;
                k++;
                return;
            }
            num ^= basis[i];
        }
        // 如果 num 最终变为 0,说明 num 可以由基底中的数异或表示
        // 这意味着原集合中存在线性相关的向量,可以异或出 0
        has_zero = true;
    }

    long long queryMax() const {
        long long ans = 0;
        for (int i = BITS; i >= 0; i--) {
            ans = max(ans, ans ^ basis[i]);
        }
        return ans;
    }
    
    /**
     * @brief 查询线性基能表示的最小非零异或和。
     *
     * @return 最小非零异或和。如果线性基为空,返回 0。
     */
    long long queryMin() const {
        if(k == 0) return 0; // 如果线性基为空,没有非零异或和
        for(int i = 0; i <= BITS; ++i) {
            if(basis[i] != 0) {
                return basis[i];
            }
        }
        return 0; // 理论上不会执行到这里,除非 k 的维护有问题
    }


    //判断一个数是否可以被当前线性基中的数异或表示出来。
    bool can_xor_out(long long num) const {
        for (int i = BITS; i >= 0; i--) {
            if (((num >> i) & 1)) {
                num ^= basis[i];
            }
        }
        return num == 0;
    }


    /**
     * @brief 将线性基转换为简化行阶梯形式。
     * 这一步是为了高效地查询第 k 小值。
     * @return 转换后的基底向量数组。
     */
    vector<long long> getSimplifiedBasis() const {
        vector<long long> temp_basis;
        for(int i = 0; i <= BITS; ++i){
            if(basis[i] != 0){
                temp_basis.push_back(basis[i]);
            }
        }
        // 高斯消元,转换成对角矩阵形式,方便查询第 k 小
        for (size_t i = 0; i < temp_basis.size(); ++i) {
            for (size_t j = i + 1; j < temp_basis.size(); ++j) {
                temp_basis[j] = min(temp_basis[j], temp_basis[j] ^ temp_basis[i]);
            }
        }
        return temp_basis;
    }
    /**
     * @brief 查询线性基能表示的第 rank 小的异或和。
     *
     * @param rank 排名 (从 1 开始)。
     * @return 第 rank 小的异或和。如果 rank 超出范围,返回 -1。
     */
    long long queryKth(long long rank) const {
        // 如果原数集可以异或出 0,那么第 1 小是 0。
        // 我们需要调整 rank 来查询非零异或和中的第 (rank-1) 小。
        if (has_zero) rank--;
        // 如果调整后的 rank 是 0,说明查询的是第 1 小,且 0 可以被凑出。
        if (rank == 0) return 0;
        // 能表示出的不同异或和的数量是 2^k,如果 rank 超出范围,则无法表示。
        if (rank >= (1LL << k)) return -1;

        vector<long long> simplified_basis = getSimplifiedBasis();

        long long ans = 0;
        for (size_t i = 0; i < simplified_basis.size(); ++i) {
            if ((rank >> i) & 1) {
                ans ^= simplified_basis[i];
            }
        }
        return ans;
    }
};

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

    vector<int> a(n);

    for(int i=0;i<n;i++){
        cin>>a[i];
    }

    int ans=0;

    auto dfs=[&](auto dfs,int now,XorBasis &xb)->void {
        ans=max(ans,xb.queryMax());

        if(now+2<n){
            XorBasis nxb=xb;
            nxb.insert(a[now+2]);
            dfs(dfs,now+2,nxb);
        }

        if(now+3<n){
            xb.insert(a[now+3]);
            dfs(dfs,now+3,xb);
        }
    };  


    for(int i=1;i<=2;i++){//至少有 i 个元素
        if(n>=i){
            XorBasis xb;
            xb.insert(a[i-1]);
            dfs(dfs,i-1,xb);
        }
    }

    cout<<ans<<endl;
}

signed main(){
    ios::sync_with_stdio(0);
    cin.tie(0);

    int ct=1;
    cin>>ct;
    while(ct--){
        solve();
    }
    return 0;
}
posted @ 2025-07-25 11:43  LYET  阅读(93)  评论(0)    收藏  举报