Logo wfirstzhang 的博客

博客

#808. 三子串拼接回文计数 题目资源

avatar wfirstzhang 2026-10-15 19:40:16

回文拼接:参考算法与正确性

本文为出题人文档,不放入选手附件。下标从 0 开始,X[i:e] 表示左闭右开的连续子串。

回文起点计数

定义 $p_X(i)$ 为字符串 $X$ 中从 $i$ 开始的非空回文子串数,并令 $p_X(|X|)=0$。

Manacher 求出奇偶回文半径。一个奇中心 $c$、半径 $r$ 对起点区间 $[c-r+1,c]$ 各贡献 1;一个偶中心 $c$、半径 $r$ 对 $[c-r,c-1]$ 各贡献 1。用差分得到整个 $p_X$,时间和空间均为线性。

两串统计

令 $Y$ 为第二个字符串的反串。对第一个字符串 $X$ 的每个起点 $i$,求 $R_{X,Y}(i)$:从 $i$ 开始在 $X$ 中取一个非空子串,并在原第二串中取一个非空子串,使它们拼接为回文的方案数。约定 $R_{X,Y}(|X|)=0$。

设两段长度的较小值为 $\ell$。剥掉相等的外层,必然得到

$$ X[i:i+\ell]=Y[j:j+\ell]. $$

余下的部分恰好属于下列互斥情况之一:为空;是 $X$ 中从 $i+\ell$ 开始的非空回文;是 $Y$ 中从 $j+\ell$ 开始的非空回文。因此

$$ R_{X,Y}(i)=\sum_{\ell\ge1}\sum_{j:\,X[i:i+\ell]=Y[j:j+\ell]} \bigl(1+p_X(i+\ell)+p_Y(j+\ell)\bigr). $$

较短段的长度与外层匹配位置由原方案唯一确定,故没有重计,也不会计入空的原子串。

分块求全部 $R$

把 $X$ 分成长度 $B$ 的块;最后一块可以较短。每对相同子串,按其在 $X$ 中的匹配段是否跨过起点所在块的右边界分类。

匹配段留在块内

每个起点最多枚举 $B$ 个长度。在 $Y$ 上建 SAM,每个状态维护:

  • occ:结束位置出现次数;
  • weighted:所有结束位置 $e$ 的 $p_Y(e+1)$ 之和。

这两个量均按 suffix link 从长到短传播。沿 SAM 读取 $X[i:e]$,到达状态 $v$ 时,该匹配段的贡献就是

$$ (1+p_X(e))\operatorname{occ}(v)+\operatorname{weighted}(v). $$

时间为 $O(|X|B+|Y|)$。

匹配段跨过块边界

固定起点所在块的右边界 $b$。枚举 $Y$ 中与之对应的分割点 $j$,其中 $1\le j<|Y|$。

令 $l$ 为两边向左匹配的长度,截断至前一块长度;令 $r$ 为向右匹配的长度。用两次 Z 算法求出所有 $j$ 的 $l,r$。右侧模式只需保留至多 $|Y|$ 个字符,左侧模式只需保留至多 $\min(B,|Y|)$ 个字符,所以一个边界只需 $O(|Y|)$ 时间。

对于所有

$$ i\in[b-l,b), $$

左段均能匹配。右段长度可独立选取 $1\dots r$。无论 $i$ 取什么,其贡献都是

$$ w=\sum_{t=1}^{r}\bigl(1+p_X(b+t)+p_Y(j+t)\bigr). $$

用 $p_X,p_Y$ 的前缀和计算 $w$,对 $R[b-l:b]$ 做一次差分区间加。空匹配不贡献。

唯一归属: 一个跨块匹配段只由其起点所在块的右边界处理。它即使跨过更多块,也不会由之后的边界再次处理,因为之后边界的允许起点仅限其紧邻的前一块。

总时间为

$$ O\left(|Y|+|X|B+\frac{|X||Y|}{B}\right). $$

取 $B=\Theta(\sqrt{|Y|})$,若其大于 $|X|$ 就直接把 $X$ 作为一块。实现取约 $\sqrt{6|Y|}$ 以平衡常数。

回到三串

令 $D=\operatorname{rev}(C)$,计算

$$ F_i=R_{A,\operatorname{rev}(B)}(i),\qquad G_j=R_{D,B}(j), $$

以及 $P_B=\sum_i p_B(i)$。

比较原方案中 $|S_A|$ 与 $|S_C|$。剥掉较短者对应的外层匹配后:

  • 等长:仅剩 $B$ 中的非空回文,贡献 $P_B$;
  • $A$ 较长:剩余是 $A$ 的一段与 $B$ 的一段拼出的回文,贡献 $F$;
  • $C$ 较长:反转剩余部分后,贡献 $G$。

定义 $K_A(e)$:$A,D$ 中一对相同非空子串的方案数,要求其中 $A$ 的子串以 $e$ 结束。对称定义 $K_D(f)$。令

$$ K=\sum_e K_A(e)=\sum_f K_D(f). $$

最终答案为

$$ P_BK+\sum_e K_A(e)F_{e+1}+\sum_f K_D(f)G_{f+1}\pmod {998244353}. $$

三种外层长短关系互斥,外层匹配的位置和长度也唯一,故该公式恰好统计每个合法方案一次。

线性计算 $K$

在目标串上建 SAM 并传播 occ。令

$$ H(v)=H(\operatorname{link}(v))+ (\operatorname{len}(v)-\operatorname{len}(\operatorname{link}(v)))\operatorname{occ}(v). $$

扫描另一个串。当当前最长匹配后缀长度为 $q$、对应状态为 $v$ 时,所有匹配后缀的出现次数之和为

$$ H(\operatorname{link}(v))+ (q-\operatorname{len}(\operatorname{link}(v)))\operatorname{occ}(v). $$

这就是该结束位置的 $K$。两个方向各扫描一次即可。

复杂度与整数范围

设 $L=\max(|A|,|B|,|C|)$。总时间 $O(L\sqrt L)$、额外空间 $O(L)$。多次调用的总时间不超过

$$ O\left(\sqrt{\max_i L_i}\sum_i L_i\right). $$

在 $L\le10^5$ 时,$p$ 的前缀和、单个 $R_i$、单个 $K_i$ 与 $K$ 总和均可用有符号 64 位整数保存。但最终公式中的乘积可能远超 64 位,必须先将乘数分别取模,再相乘。模数小于 $10^9$,两模数内整数的乘积可用 64 位整数保存。

所有数据结构都在一次函数调用内重新建立。空的残余串只体现在两串公式中的常数 1,三个被选子串始终要求非空。

gen.cpp

#include "testlib.h"
#include "plan.h"
#include "reference.h"
#include <iostream>

int main(int argc,char**argv) {
    registerGen(argc,argv,1);
    int id=opt<int>("case",1);
    int sample=opt<int>("sample",0);
    long long seed=opt<long long>("seed",20261015);
    ensuref(sample?1<=sample&&sample<=4:1<=id&&id<=64,"invalid point number");
    std::uint64_t base=(std::uint64_t)seed ^ (std::uint64_t)rnd.next(1LL,4611686018427387903LL);
    auto cases=pld_plan::make(sample?sample:id,base,sample!=0);
    std::cout<<"PLD1\n"<<cases.size()<<'\n';
    for(auto&s:cases) {
        auto c=pld_cases::make(s);
        s.answer=int(pld_reference::count_impl(c.a,c.b,c.c));
        std::cout<<s.kind<<' '<<s.na<<' '<<s.nb<<' '<<s.nc<<' '<<s.param<<' '<<s.flags<<' '<<s.seed<<' '<<s.answer<<'\n';
        if(s.kind==0)std::cout<<s.a<<'\n'<<s.b<<'\n'<<s.c<<'\n';
    }
}

reference.h

#ifndef PLD_REFERENCE_H
#define PLD_REFERENCE_H
#include <bits/stdc++.h>
namespace pld_reference {
using namespace std;
using i64 = long long;
const i64 MOD = 998244353;

vector<int> pal_starts(const string& s) {
    int n = (int)s.size();
    vector<int> odd(n), even(n), diff(n + 1), out(n + 1);
    for (int i = 0, l = 0, r = -1; i < n; ++i) {
        int k = i > r ? 1 : min(odd[l + r - i], r - i + 1);
        while (i - k >= 0 && i + k < n && s[i-k] == s[i+k]) ++k;
        odd[i] = k;
        ++diff[i-k+1]; --diff[i+1];
        if (i+k-1 > r) l = i-k+1, r = i+k-1;
    }
    for (int i = 0, l = 0, r = -1; i < n; ++i) {
        int k = i > r ? 0 : min(even[l + r - i + 1], r - i + 1);
        while (i-k-1 >= 0 && i+k < n && s[i-k-1] == s[i+k]) ++k;
        even[i] = k;
        if (k) ++diff[i-k], --diff[i];
        if (i+k-1 > r) l = i-k, r = i+k-1;
    }
    int carry = 0;
    for (int i = 0; i < n; ++i) out[i] = (carry += diff[i]);
    return out;
}

struct SAM {
    struct State {
        int next[26], link, len, occ;
        i64 weighted, cumulative;
        State() : link(-1), len(0), occ(0), weighted(0), cumulative(0) {
            fill(next, next+26, -1);
        }
    };
    vector<State> st;
    explicit SAM(const string& s, const vector<int>* weights = nullptr) {
        st.reserve(s.size()*2 + 1); st.emplace_back();
        int last = 0;
        for (int i = 0; i < (int)s.size(); ++i) {
            int c = s[i]-'a', cur = (int)st.size();
            st.emplace_back(); st[cur].len = st[last].len + 1;
            st[cur].occ = 1;
            if (weights) st[cur].weighted = (*weights)[i+1];
            int p = last;
            while (p != -1 && st[p].next[c] == -1) {
                st[p].next[c] = cur; p = st[p].link;
            }
            if (p == -1) st[cur].link = 0;
            else {
                int q = st[p].next[c];
                if (st[p].len + 1 == st[q].len) st[cur].link = q;
                else {
                    int clone = (int)st.size();
                    st.push_back(st[q]); st[clone].len = st[p].len + 1;
                    st[clone].occ = 0; st[clone].weighted = 0;
                    while (p != -1 && st[p].next[c] == q) {
                        st[p].next[c] = clone; p = st[p].link;
                    }
                    st[q].link = st[cur].link = clone;
                }
            }
            last = cur;
        }
        vector<int> cnt(s.size()+1), order(st.size());
        for (auto& v : st) ++cnt[v.len];
        for (int i = 1; i <= (int)s.size(); ++i) cnt[i] += cnt[i-1];
        for (int v = (int)st.size()-1; v >= 0; --v) order[--cnt[st[v].len]] = v;
        for (int k = (int)order.size()-1; k > 0; --k) {
            int v = order[k], p = st[v].link;
            st[p].occ += st[v].occ; st[p].weighted += st[v].weighted;
        }
        for (int k = 1; k < (int)order.size(); ++k) {
            int v = order[k], p = st[v].link;
            st[v].cumulative = st[p].cumulative + (i64)(st[v].len-st[p].len)*st[v].occ;
        }
    }
    vector<i64> common_suffix(const string& x) const {
        vector<i64> out(x.size());
        int v = 0, len = 0;
        for (int i = 0; i < (int)x.size(); ++i) {
            int c = x[i]-'a';
            while (v && st[v].next[c] == -1) {
                v = st[v].link; len = st[v].len;
            }
            if (st[v].next[c] == -1) v = 0, len = 0;
            else v = st[v].next[c], ++len;
            if (v) {
                int p = st[v].link;
                out[i] = st[p].cumulative + (i64)(len-st[p].len)*st[v].occ;
            }
        }
        return out;
    }
};

vector<int> zfunc(const string& s) {
    int n = (int)s.size(); vector<int> z(n);
    for (int i = 1, l = 0, r = 0; i < n; ++i) {
        if (i < r) z[i] = min(r-i, z[i-l]);
        while (i+z[i] < n && s[z[i]] == s[i+z[i]]) ++z[i];
        if (i+z[i] > r) l = i, r = i+z[i];
    }
    return z;
}

vector<i64> pair_rows(const string& x, const string& y,
                      const vector<int>& px, const vector<int>& py, int block) {
    int n = (int)x.size(), m = (int)y.size();
    if (!block) block = max(1, min(n, (int)sqrt(6.0*m)));
    vector<i64> out(n+1), diff(n+1), sx(n+2), sy(m+2);
    for (int i = 0; i <= n; ++i) sx[i+1] = sx[i] + px[i];
    for (int i = 0; i <= m; ++i) sy[i+1] = sy[i] + py[i];
    {
        SAM sam(y, &py);
        for (int i = 0; i < n; ++i) {
            int stop = min(n, (i/block+1)*block), v = 0;
            for (int e = i; e < stop; ++e) {
                v = sam.st[v].next[x[e]-'a'];
                if (v == -1) break;
                out[i] += (i64)(1+px[e+1])*sam.st[v].occ + sam.st[v].weighted;
            }
        }
    }
    if (m > 1) {
        string ry(y.rbegin(), y.rend());
        for (int b = block; b < n; b += block) {
            int plen = min(n-b, m), llen = min({b,block,m});
            string text = x.substr(b, plen); text += '#'; text += y;
            auto right = zfunc(text);
            text = x.substr(b-llen, llen); reverse(text.begin(), text.end());
            text += '#'; text += ry;
            auto left = zfunc(text);
            for (int j = 1; j < m; ++j) {
                int a = left[llen+1+m-j], r = right[plen+1+j];
                if (!a || !r) continue;
                i64 w = r + sx[b+r+1]-sx[b+1] + sy[j+r+1]-sy[j+1];
                diff[b-a] += w; diff[b] -= w;
            }
        }
    }
    i64 carry = 0;
    for (int i = 0; i < n; ++i) out[i] += (carry += diff[i]);
    return out;
}

i64 count_impl(const string& a, const string& b, const string& c, int block=0) {
    string rb(b.rbegin(),b.rend()), rc(c.rbegin(),c.rend());
    auto pa = pal_starts(a), pb = pal_starts(b);
    auto prb = pal_starts(rb), prc = pal_starts(rc);
    auto ra = pair_rows(a,rb,pa,prb,block);
    auto rr = pair_rows(rc,b,prc,pb,block);
    auto ka = SAM(rc).common_suffix(a);
    auto kr = SAM(a).common_suffix(rc);
    i64 palb = accumulate(pb.begin(),pb.end(),0LL);
    i64 common = accumulate(ka.begin(),ka.end(),0LL);
    i64 ans = (palb%MOD)*(common%MOD)%MOD;
    for (int i = 0; i < (int)a.size(); ++i) ans = (ans+(ka[i]%MOD)*(ra[i+1]%MOD))%MOD;
    for (int i = 0; i < (int)rc.size(); ++i) ans = (ans+(kr[i]%MOD)*(rr[i+1]%MOD))%MOD;
    return ans;
}

}
#endif

plan.h

#ifndef PLD_PLAN_H
#define PLD_PLAN_H
#include "require/case_generator.h"
#include <algorithm>
#include <vector>

namespace pld_plan {
using pld_cases::Spec;
using pld_cases::RNG;

inline std::vector<Spec> make(int id, std::uint64_t seed, bool sample=false) {
    std::vector<Spec> v;int budget=0;RNG rng(seed);
    auto literal=[&](std::string a,std::string b,std::string c) {
        Spec s={0,int(a.size()),int(b.size()),int(c.size()),0,0,0,0,a,b,c};
        budget+=pld_cases::length(s);v.push_back(s);
    };
    auto add=[&](int k,int a,int b,int c,int p=3,int f=2,std::uint64_t fixed=0) {
        Spec s={k,a,b,c,p,f,fixed?fixed:(rng.next()&UINT64_C(0x7fffffffffffffff)),0,"","",""};
        int l=pld_cases::length(s);if(budget+l>100000)return;
        budget+=l;v.push_back(s);
    };
    if(sample) {
        if(id==1)literal("a","b","c");
        if(id==2)literal("ab","ba","ab");
        if(id==3)literal("ab","ab","ab");
        if(id==4)literal("aa","aa","aa");
        return v;
    }
    if(id==1) {
        std::vector<std::string>words;
        for(int n=1;n<=3;++n)for(int mask=0;mask<(1<<n);++mask) {
            std::string s(n,'a');for(int i=0;i<n;++i)s[i]+=((mask>>i)&1);words.push_back(s);
        }
        for(auto&a:words)for(auto&b:words)for(auto&c:words)literal(a,b,c);
    } else if(id==2) {
        for(int i=0;i<12000;++i)add(3,rng.integer(1,8),rng.integer(1,8),rng.integer(1,8),rng.integer(1,4));
    } else if(id==3) {
        for(int k=1;k<=20;++k)for(int z=0;z<40;++z)add(k,rng.integer(1,8),rng.integer(1,8),rng.integer(1,8),k==11?3:3+z%5,z%4);
    } else if(id==4) {
        for(int a=1;a<=8;++a)for(int b=1;b<=8;++b)for(int c=1;c<=8;++c)add(1,a,b,c,0,2);
    } else if(id==5) {
        const char* triples[][3]={{"a","a","a"},{"a","b","a"},{"ab","ab","ab"},{"ab","ba","ab"},
          {"aa","aa","aa"},{"abcba","c","a"},{"a","bc","cba"},{"abc","b","cba"},
          {"abba","aa","abba"},{"abaabb","baba","bbabaa"},{"z","a","z"},{"zzzzzzzz","zz","zzzzzzz"}};
        for(auto&t:triples)literal(t[0],t[1],t[2]);
        for(int i=0;i<6000;++i)add(3,rng.integer(1,8),rng.integer(1,8),rng.integer(1,8),2+i%3,i%4);
    } else if(id==6) {
        for(int i=0;i<100000;++i)add(3,1,1,1,i%3==0?1:i%3==1?2:26,i%4);
    } else if(id==7) {
        for(int i=0;i<2000;++i) {
            std::uint64_t s=rng.next()&UINT64_C(0x7fffffffffffffff);
            int n=1+i%8;add(3,n,n,n,2,0,s);add(3,1,1,1,26,2);
            add(3,n,n,n,2,0,s);add(3,n,n,n,2,1,s);add(3,n,n,n,2,2,s);
        }
    } else if(id==8) {
        for(char a='a';a<='z';++a)for(char b='a';b<='z';++b) {
            literal(std::string(1,a),std::string(1,b),std::string(1,a));
            literal(std::string(2,a),std::string(3,b),std::string(2,b));
        }
    } else if(id==9) {
        for(int i=0;i<100;++i)add(3,1000,1000,1000,i%3==0?2:i%3==1?3:26);
    } else if(id==10) {
        for(int i=0;i<100;++i)add(i%2?5:6,1000,999-i%5,1000,1+i%37,i%4);
    } else if(id==11) {
        for(int i=0;i<100;++i)add(12+i%3,1000,1000-i%5,997+i%4,i%2,i%4);
    } else if(id==12) {
        const int ns[]={1,2,3,15,16,17,31,32,33,63,64,65,127,128,129,255,256,257,316,317,318,447,448,449,511,512,513,773,774,775,776,999,1000};
        for(int round=0;round<7;++round)for(int n:ns)add(round%2?6:16,n,std::max(1,n-1),n,round%2?7:31,round%4);
    } else if(id==13) {
        const int thin[]={1,2,3,7,31,127};
        for(int i=0;i<100;++i) {
            int q=thin[i%6],axis=i%3;
            add(4+i%3,axis==0?q:1000,axis==1?q:1000,axis==2?q:1000,2+i%7,i%4);
        }
    } else if(id==14) {
        for(int i=0;i<1600;++i)add(3,1+i%60,1+(i*37)%60,1+(i*19)%60,1+i%6,i%4);
    } else if(id==15) {
        for(int i=0;i<100;++i)add(9+i%12,1000,1000,1000,i%12==2?8:31,i%4);
    } else if(id==16) {
        while(budget<100000){int n=std::min(100000-budget,rng.integer(1,1000));add(1+rng.integer(0,19),n,rng.integer(1,n),rng.integer(1,n),7,rng.integer(0,3));}
    } else if(id==17) {
        add(1,100000,100000,100000,0,2);
    } else if(id==18) {
        for(int n:{70000,20000,9000,998,1,1})add(1,n,std::max(1,n/2),std::max(1,n-7),0,2);
    } else if(id==19) {
        add(1,99991,99989,99990,0,2);add(1,1,9,2,0,2);
    } else if(id==20) {
        for(int i=0;i<100000;++i)add(1,1,1,1,0,2);
    } else if(id>=21 && id<=40) {
        int k=id-20,p=3;
        if(k==5 || k==6)p=317;
        if(k==7)p=769;
        if(k==8)p=774;
        if(k==11)p=15;
        if(k==14)p=1;
        if(k==16)p=1023;
        if(k==17)p=26;
        add(k,100000,100000,100000,p,2);
    } else if(id>=41 && id<=52) {
        const int ps[]={2,7,31,127,313,317,509,769,774,775,997,1024};
        int j=id-41;add(j%2?6:5,100000,99999-j,99997-j,ps[j],j%4);
    } else if(id==53) {
        add(17,100000,1,99999,2,2);
    } else if(id==54) {
        add(4,1,100000,99991,3,2);
    } else if(id==55) {
        add(6,99991,100000,1,31,2);
    } else if(id==56) {
        add(19,2,100000,3,2,2);
    } else if(id==57) {
        const int ns[]={40000,1,30000,7,20000,9992};
        for(int i=0;i<6;++i)add(i%2?3:16,ns[i],ns[i],std::max(1,ns[i]-1),i%2?26:313,i%4);
    } else if(id==58) {
        for(int i=0;i<4;++i)add(8+i*3,25000,24999-i,25000,i==1?14:127,i%4);
    } else if(id==59) {
        while(budget<100000){int n=std::min(100000-budget,rng.integer(1,3000));add(5+rng.integer(0,15),n,std::max(1,n-3),std::max(1,n-7),31,rng.integer(0,3));}
    } else if(id==60) {
        add(12,50000,49999,49997,3,2);add(13,50000,49997,49999,3,2);
    } else if(id==61) {
        add(16,50000,50000,50000,4093,2);add(6,50000,50000,50000,4093,2);
    } else if(id==62) {
        std::uint64_t s=rng.next()&UINT64_C(0x7fffffffffffffff);
        add(16,50000,49991,49993,509,0,s);add(16,50000,49991,49993,509,1,s);
    } else if(id==63) {
        std::uint64_t s=rng.next()&UINT64_C(0x7fffffffffffffff);
        for(int f=0;f<4;++f)add(6,25000,24989,24997,317,f,s);
    } else if(id==64) {
        for(int k:{4,8,16,18})add(k,25000,24999,24997,k==16?1023:3,2);
    }
    if(v.empty() || budget>100000)throw std::runtime_error("invalid plan");
    return v;
}
}
#endif

case_generator.h

#ifndef PLD_CASE_GENERATOR_H
#define PLD_CASE_GENERATOR_H
#include <algorithm>
#include <cstdint>
#include <stdexcept>
#include <string>
#include <vector>

namespace pld_cases {
struct Spec {
    int kind, na, nb, nc, param, flags;
    std::uint64_t seed;
    int answer;
    std::string a, b, c;
};
struct Case { std::string a, b, c; };

// The exact unsigned operations below specify a portable, reproducible PRNG.
class RNG {
    std::uint64_t state;
public:
    explicit RNG(std::uint64_t seed) : state(seed) {}
    std::uint64_t next() {
        std::uint64_t z = (state += UINT64_C(0x9e3779b97f4a7c15));
        z = (z ^ (z >> 30)) * UINT64_C(0xbf58476d1ce4e5b9);
        z = (z ^ (z >> 27)) * UINT64_C(0x94d049bb133111eb);
        return z ^ (z >> 31);
    }
    int integer(int l, int r) { return l + int(next() % (std::uint64_t(r-l)+1)); }
};
inline std::string reversed(std::string x) {
    std::reverse(x.begin(),x.end()); return x;
}
inline std::string random_word(int n, int alphabet, RNG &rng, int shift=0) {
    std::string s(n,'a');
    for (char &c:s) c=char('a'+shift+rng.integer(0,alphabet-1));
    return s;
}
inline std::string cycle(const std::string &s, int n, int shift=0) {
    std::string t(n,'a');
    for(int i=0;i<n;++i)t[i]=s[(i+shift)%s.size()];
    return t;
}
inline std::string mirror_word(int n, int alphabet, RNG &rng, int shift=0) {
    std::string s=random_word(n,alphabet,rng,shift);
    for(int i=0;i<n/2;++i)s[n-1-i]=s[i];
    return s;
}
inline std::string runs(int n,int length,RNG &rng) {
    std::string s; s.reserve(n); int letter=rng.integer(0,2);
    while(int(s.size())<n) {
        int take=std::min(n-int(s.size()),rng.integer(std::max(1,length/2),std::max(1,length*3/2)));
        s.append(take,char('a'+letter));letter=(letter+rng.integer(1,2))%3;
    }
    return s;
}
inline std::string debruijn(int order) {
    std::vector<int>a(2*order+2);std::string s;
    struct Rec {
        static void go(int t,int p,int k,std::vector<int>&a,std::string&s) {
            if(t>k){if(k%p==0)for(int i=1;i<=p;++i)s+=char('a'+a[i]);return;}
            a[t]=a[t-p];go(t+1,p,k,a,s);
            for(int j=a[t-p]+1;j<2;++j){a[t]=j;go(t+1,t,k,a,s);}
        }
    };
    Rec::go(1,1,order,a,s);return s;
}
inline Case make(const Spec &s) {
    if(s.kind==0)return {s.a,s.b,s.c};
    RNG rng(s.seed);Case t;
    int n=std::max({s.na,s.nb,s.nc}),p=std::max(1,s.param);
    if(s.kind==1) {
        char c=char('a'+rng.integer(0,25));
        t={std::string(s.na,c),std::string(s.nb,c),std::string(s.nc,c)};
    } else if(s.kind==2) {
        t={std::string(s.na,'a'),std::string(s.nb,'b'),std::string(s.nc,'c')};
    } else if(s.kind==3) {
        p=std::min(26,p);
        t={random_word(s.na,p,rng),random_word(s.nb,p,rng),random_word(s.nc,p,rng)};
    } else if(s.kind==4) {
        std::string base=random_word(n+97,std::min(26,p),rng);
        t={cycle(base,s.na,0),reversed(cycle(base,s.nb,13)),reversed(cycle(base,s.nc,31))};
    } else if(s.kind==5 || s.kind==6) {
        std::string period=random_word(std::min(p,n),3,rng);
        t={cycle(period,s.na,0),reversed(cycle(period,s.nb,rng.integer(0,int(period.size())-1))),
           reversed(cycle(period,s.nc,rng.integer(0,int(period.size())-1)))};
        if(s.kind==6)for(std::string *v:{&t.a,&t.b,&t.c}) {
            int count=1+rng.integer(0,3);
            for(int z=0;z<count;++z) {
                int i=z==0?int(v->size()/2):z==1?0:z==2?int(v->size()-1):rng.integer(0,int(v->size())-1);
                (*v)[i]=char('a'+((*v)[i]-'a'+rng.integer(1,2))%3);
            }
        }
    } else if(s.kind==7) {
        t={runs(s.na,p,rng),runs(s.nb,p,rng),runs(s.nc,p,rng)};
    } else if(s.kind==8) {
        t={std::string(s.na,'a'),std::string(s.nb,'a'),std::string(s.nc,'a')};
        for(std::string *v:{&t.a,&t.b,&t.c}) {
            int choice=rng.integer(0,3),len=int(v->size());
            int i=choice==0?0:choice==1?len-1:choice==2?len/2:s.param%len;
            (*v)[i]='b';
        }
    } else if(s.kind==9) {
        std::string base(n+97,'a');
        for(int i=0;i<int(base.size());++i) {
            unsigned x=unsigned(i);int parity=0;
            while(x){parity^=1;x&=x-1;}base[i]=char('a'+parity);
        }
        t={cycle(base,s.na),reversed(cycle(base,s.nb,13)),reversed(cycle(base,s.nc,31))};
    } else if(s.kind==10) {
        std::string a="a",b="ab";
        while(int(b.size())<n+97){std::string c=b+a;a=b;b=c;}
        t={cycle(b,s.na),reversed(cycle(b,s.nb,13)),reversed(cycle(b,s.nc,31))};
    } else if(s.kind==11) {
        int order=1;while(order<16 && (1<<order)<n)++order;
        std::string base=debruijn(std::min(order,p));
        t={cycle(base,s.na),reversed(cycle(base,s.nb,13)),reversed(cycle(base,s.nc,31))};
    } else if(s.kind==12 || s.kind==13) {
        t={random_word(s.na,3,rng),random_word(s.nb,3,rng,3),random_word(s.nc,3,rng)};
        if(s.kind==12 && s.na>=3) {
            int u=std::min(s.nc,s.na/3),v=std::min(s.nb,s.na/3);
            int ci=(s.nc-u)/2,bi=(s.nb-v)/2;
            t.a=reversed(t.c.substr(ci,u))+reversed(t.b.substr(bi,v))+mirror_word(s.na-u-v,3,rng,6);
        }
        if(s.kind==13 && s.nc>=3) {
            int u=std::min(s.na,s.nc/3),v=std::min(s.nb,s.nc/3);
            int ai=(s.na-u)/2,bi=(s.nb-v)/2;
            t.c=mirror_word(s.nc-u-v,3,rng,6)+reversed(t.b.substr(bi,v))+reversed(t.a.substr(ai,u));
        }
    } else if(s.kind==14) {
        t={std::string(s.na,'a'),s.param==0?std::string(s.nb,'b'):random_word(s.nb,2,rng,1),std::string(s.nc,'a')};
    } else if(s.kind==15) {
        t={random_word(s.na,3,rng),random_word(s.nb,26,rng),random_word(s.nc,3,rng,23)};
    } else if(s.kind==16) {
        p=std::min(p,n);
        std::vector<std::string>pool;
        for(int i=0;i<7;++i)pool.push_back(random_word(p,3,rng));
        auto stream=[&](int len,char separator) {
            std::string v;v.reserve(len+p+1);
            while(int(v.size())<len){v+=pool[rng.integer(0,6)];v+=separator;}
            v.resize(len);return v;
        };
        t={stream(s.na,'x'),reversed(stream(s.nb,'y')),reversed(stream(s.nc,'z'))};
    } else if(s.kind==17) {
        std::string base=random_word(n,std::min(26,p),rng);
        t={cycle(base,s.na),mirror_word(s.nb,std::min(26,p),rng),reversed(cycle(base,s.nc))};
        int i=rng.integer(0,s.nc-1);t.c[i]=char('a'+(t.c[i]-'a'+1)%26);
    } else if(s.kind==18) {
        std::string base="a";int level=1;
        while(int(base.size())<n+97){base=base+char('a'+level%26)+base;++level;}
        t={cycle(base,s.na),reversed(cycle(base,s.nb,13)),reversed(cycle(base,s.nc,31))};
    } else if(s.kind==19) {
        t={mirror_word(s.na,std::min(26,p),rng),mirror_word(s.nb,std::min(26,p),rng),mirror_word(s.nc,std::min(26,p),rng)};
    } else if(s.kind==20) {
        std::string base;
        for(int k=1;int(base.size())<n+97;k=k>=4096?1:k*2) {
            base+=std::string(k,'a');base+='b';base+=std::string(k,'a');base+='c';
        }
        t={cycle(base,s.na),reversed(cycle(base,s.nb,13)),reversed(cycle(base,s.nc,31))};
    } else throw std::runtime_error("unknown generator kind");
    if(s.flags&1)t={reversed(t.c),reversed(t.b),reversed(t.a)};
    if(s.flags&2) {
        std::string letters="abcdefghijklmnopqrstuvwxyz";
        for(int i=25;i>0;--i)std::swap(letters[i],letters[rng.integer(0,i)]);
        for(std::string *v:{&t.a,&t.b,&t.c})for(char &c:*v)c=letters[c-'a'];
    }
    return t;
}
inline int length(const Spec&s){return std::max({s.na,s.nb,s.nc});}
}
#endif

WyOJ Contest 60 题面(直接喂 AI)

avatar wfirstzhang 2026-10-15 09:01:47
A
时间限制 1s,内存限制 512MiB。

题目描述
你有一个长度为 n的序列 ai 和两个数 x,y。
现在你需要选择两个不相交的区间 [l1,r1],[l2,r2],将 [l1,r1] 中的所有数按位或上 x,[l2,r2]中的所有数按位或上 y。
注意,区间长度至少为 1。
求所有元素的和的可能最大值。

输入格式
本题使用多组数据。
第一行一个正整数 t 表示数据组数。
对于每组数据:第一行三个整数 n,x,y,含义见题目描述。
第二行 n 个整数 a1,a2,⋯,an表示序列 a。
1≤t≤5,2≤n≤5×10^5,0≤x,y,ai<2^30。
每个测试点 n 的总和不超过 5×10^5。

输出格式
对于每组数据,输出一行一个整数表示操作后元素和的最大值。

输入输出样例 #1
输入 #1
2
4 1 2
4 3 6 7
5 3 4
7 5 8 2 4
输出 #1
23
37
说明/提示
对于第 1组数据,一种最优解是选择 l1=r1=3,l2=r2=1,操作后序列变为 [6,3,7,7],元素和为 23,可以证明这是元素和的最大可能值。

---

B
时间限制 2s,内存限制 512MiB。

题目描述
给定一个长度为 n的序列 a,求这个序列是否能被划分为若干个连续段(数量不为 1),使每一段中的所有数的异或和均相等。

输入格式
多测,第一行输入数据组数 T。
对于每一组数据:
第一行一个整数 n。第二行 n个整数表示数组 a。
1≤T≤100,2≤n≤2×10^5,0≤ai≤10^9。
每个测试点 n 的总和不超过 2×10^5。

输出格式
如果这个序列满足要求的,输出 Yes,否则输出 No。
每组数据之间以换行分隔。

输入输出样例 #1
输入 #1
3
7
5 2 0 1 3 1 4
10
2 2 3 3 4 4 5 5 6 6
2
2 4
输出 #1
Yes
Yes
No
说明/提示
对于第 1 组数据,可以划分为 [5,2,0],[1,3,1,4]。对于第 2组数据,可以划分为 [2,2],[3,3],[4,4],[5,5],[6,6]。对于第 3组数据,可以证明无法满足要求。

---

C
时间限制 1s,内存限制 512MiB。

题目描述
从一个长度为 n的序列中找出一个子序列为公差不为 0的长度为 k的等差数列,保证有解(k=1时任意一个元素组成的数列都是解)。请输出这个等差数列。

输入格式
多组数据。第一行一个整数 t表示数据组数。
接下来 t次重复:
第一行,两个整数 n和 k,表示序列长度与等差序列长度。
第二行,n 个整数 ai,表示这个序列。
保证 n/2≤k≤n,输入的 ai 严格单调递增。
1≤t≤5,2≤k≤n≤10^5,1≤ai≤10^9。

输出格式
一行 k个整数,表示等差数列。如果有多组解,输出任意一组。

输入输出样例 #1
输入 #1
3
3 2
1 4 5
6 4
3 4 5 6 7 9
7 4
1 3 5 6 9 11 13
输出 #1
4 5
3 5 7 9
1 5 9 13

---

D
时间限制 1s,内存限制 512MiB。

题目描述
给定一棵以 1 为根,有 n 个点的树(节点编号从 1 开始)。现在,你需要从树上选出不相同的 k 个点,令它们编号的集合为 S。
定义集合 T={lca(x,y)|x,y∈S}。请注意,x=y 是可能的。你需要最大化集合 T 的大小,并给出一组可行的构造方案。

输入格式
本题使用多组数据。
第一行一个正整数 t 表示数据组数。
对于每组数据:第一行两个整数 n,k,含义见题目描述。
第二行 n−1 个整数 f2,f3,f4,⋯,fn。其中 fi 表示点 i 的父亲是 fi。
1≤t≤10,2≤k≤n≤10^5。

输出格式
对于每组测试数据:第一行一个整数表示集合 T 的最大大小。
第二行输出 k 个整数表示你选出的 k 个点,若有多种方案,输出任意一个即可。k 个点不必按顺序输出。

输入输出样例 #1
输入 #1
2
8 3
1 1 2 2 2 3 7
10 5
1 1 2 4 3 5 7 6 6
输出 #1
5
3 4 5
7
2 3 4 9 10

OI - AIReNa调题记录

avatar wfirstzhang 2026-09-22 15:00:21

洛谷-P15073

洛谷-P11232

CodeForces-1902F

洛谷-P2746

洛谷-P1347

CodeForces-1854B

CodeForces-959D

洛谷-P16959

CodeForces-364D

洛谷-P3232

AtCoder-abc474_f

lxn AI 20260909

avatar wfirstzhang 2026-09-11 11:18:01

CSP-S 2026 提高级模拟赛(育才中学信息学集训队)试题解析与题解

对应题面合集:CSP-S-模拟赛题面合集.md;标程见各题 src/*.cpp。 说明:本套每点均分(无捆绑子任务扣分);下表“分档/子任务”为该题原题的子任务设计,仅供学习分层做法参考。满分做法均可直接 AC 本套全部数据。


〇、总体难度与考试策略

题号 题目(目录) 难度(参考) 核心考察点 建议耗时
一 染色积木(color) ★☆☆☆☆ 入门(黄) 计数、分类讨论、模拟转公式 10 min
二 棋子摆放(piece) ★★☆☆☆ 普及~提高-(绿) 排序去重、阶梯结构、计数 DP 30 min
三 不平衡划分(balance) ★★☆☆☆ 提高-(绿) 前缀和、贪心/结论、极值枚举 30 min
四 召唤小鸟(bird) ★★★☆☆ 提高(蓝/CF 2100) 背包 DP、状态设计、64 位边界 40 min
五 暗号计数(binaria) ★★★★☆ 提高+/省选-(青/蓝) 差分约束建模、并查集/类链、组合数取模 50 min

建议顺序:一 → 三 → 二 → 四 → 五。一、三尽量拿满,二若排序+DP 思路受阻先写“枚举棋盘子集/枚举行带子集”的部分分,四、五最后攻坚,注意防爆栈、防溢出。


题目一 染色积木(color)

难度与考察点

  • 难度:★☆☆☆☆(黄,签到题)。
  • 考察点:单位立方体涂色面数计数;按“厚度”分类讨论;$O(1)$ 公式;64 位整数。

题意

$a\times b\times c$ 长方体先整体刷外表面再切成单位小方块,求恰有 $k$ 个面被涂的小方块数。$a,b,c\le10^5$,$k\in[0,6]$。

满分题解($O(1)$)

把三个边长降序排序 $x\ge y\ge z$。一个单位小方块在某条长度 $L$ 的轴上,只有位于两端(坐标为 $1$ 或 $L$)时该方向才有面被涂,贡献为 $[i=1]+[i=L]$($L=1$ 时贡献 $2$)。因此只需按“厚度”分四类统计:

  1. 真三维 $z\ge2$:
    • $k=3$:$8$(角块);
    • $k=2$:$4\big((x-2)+(y-2)+(z-2)\big)=4(x+y+z-6)$(棱上非角块);
    • $k=1$:$2\big[(x-2)(y-2)+(x-2)(z-2)+(y-2)(z-2)\big]$(面内非棱块);
    • $k=0$:$(x-2)(y-2)(z-2)$(内部块);其余 $k$ 为 $0$。
  2. 平板 $z=1,y\ge2$(厚度 1 方向两面必涂,故没有 0 面块):
    • $k=4$:$4$;$k=3$:$2[(x-2)+(y-2)]$;$k=2$:$(x-2)(y-2)$;$k=0,1,5,6$:$0$。
  3. 长条 $z=1,y=1,x\ge2$:$k=5$:$2$;$k=4$:$x-2$;其余为 $0$。
  4. 单块 $1\times1\times1$:$k=6$:$1$;其余 $0$。

复杂度 $O(1)$,空间 $O(1)$。

分档(原题 20 点均分,无子任务)

  • 可直接按题意暴力:$O(a\cdot b\cdot c)$ 逐格统计涂色面数,能过 $a,b,c\le 10^2$ 的小数据(保底分);
  • 满分即上表公式。注意“平板 $k=0$ 输出 0”是最易写错的分支(厚度 1 的两面必涂)。

易错点

  • 答案可达 $\approx(10^5-2)^3\approx10^{15}$,必须 long long;
  • 排序后再分类,避免漏掉“某一维为 1/2”的形状;
  • $k=6$ 只有 $1\times1\times1$ 时才有答案。

题目二 棋子摆放(piece)

难度与考察点

  • 难度:★★☆☆☆(绿)。
  • 考察点:约束支配关系与排序去重;阶梯(单调)结构;组合计数 + 线性 DP;快速幂取模。

题意

$m\times m$ 棋盘($m\le10^9$),每格至多放 1 颗棋子;$n$($\le2\times10^5$)条规则 $(r_i,c_i)$ 表示矩形 $[1..r_i]\times[1..c_i]$ 内至多 1 颗。求方案数 $\bmod10^9+7$。

满分题解($O(n\log n)$)

Step 1 去支配,得到阶梯骨架。 若 $(r_1,c_1)\le(r_2,c_2)$(两坐标都不超过),矩形 $R_1\subseteq R_2$,$R_1$ 可删。实现:按 $r$ 降序扫描,保留 $c$ 严格递增的那些,再反转成 $r_1c_2>\dots>c_t$(代码中的 rule[])。

Step 2 覆盖格与自由格。 被覆盖格 = 落在某个规则矩形内的格,恰为 $t$ 条“行带”: 第 $i$ 带 = 行 $(r_{i-1},r_i]$($r_0=0$)× 列 $[1,c_i]$。覆盖面积 $\sum_i(r_i-r_{i-1})c_i$。自由格(其余 $m^2-$ 覆盖面积 个)上的棋子不进任何规则矩形,独立放/不放 → 因子 $2^{free}$。

Step 3 覆盖区内结构(关键引理)。 - 同一行带至多放 1 颗(否则两块同落在矩形 $R_i$ 内); - 两块分处行带 $i

Step 4 计数(链求和 + 前缀和 DP)。 一个行带组合贡献 $L_{i_1}\cdots L_{i_u}\cdot(c_{i_1}-c_{i_2})\cdots(c_{i_u}-0)$,$L_i=r_i-r_{i-1}$。对所有子集求和;用标程的前缀和递推 $O(t)$:

$$ dp[0]=1,\quad sum[i]=sum[i-1]+dp[i](r_{i+1}-r_i),\quad dp[i]=dp[i-1]+sum[i-1](c_i-c_{i+1}), $$

(哨兵 $r_{t+1}=m,\ c_{t+1}=0$),答案 $=dp[t]\cdot 2^{free}\bmod(10^9+7)$($2^{free}$ 用快速幂,指数 $\le10^{18}$)。

复杂度 $O(n\log n)$,空间 $O(n)$。

分子任务题解(原题)

子任务 限制 分档做法
1(3 分) $n\le10,m\le4$ 棋盘 $\le16$ 格,直接枚举 $2^{m^2}$ 种放法逐条规则校验。
2(6 分) $n=1,m\le1000$ 单规则 $(r,c)$:矩形 $r\cdot c$ 格内至多 1 颗 → $r\cdot c+1$ 种;外部 $2^{m^2-r\cdot c}$ → 快速幂乘出。
3–4(8+8 分) $n\le10/15$,$m$ 到 $10^9$ 去重后行带数 $t\le15$:枚举行带子集,用上列“链权值公式”$O(t\cdot2^t)$ 直接求和。
5–8(10×4) $n\le2500,m\le10^5$ 即用满分“阶梯 + 前缀和 DP”,此时 $t\le n$ 扫一遍即可($m$ 小也可退化为按行列 DP)。
9–10(15+20) $n\le2\times10^5$ 满分做法;注意 $m^2$ 用 long long、排序去重后 $t\le n$。

易错点

  • $m^2\le10^{18}$ 与 $free$ 都要 64 位;DP 与快速幂全程取模;
  • 两个哨兵 rule[0]={0,m}、rule[t+1]={m,0} 是转移边界,勿漏;
  • 完全相同的规则可重复出现,去重已自然处理。

题目三 不平衡划分(balance)

难度与考察点

  • 难度:★★☆☆☆(绿,但“结论型”,难在想到三类枚举)。
  • 考察点:前缀和、前后缀最小值、贪心/最优结构、$O(n)$ 枚举。

题意

把非负数组分成恰好 $k\ (\ge2)$ 段连续非空段,最大化(最大段和 − 最小段和)。$n\le3\times10^5$。

满分题解($O(n)$)

记 $S_i=\sum_{j\le i}a_j$,$\text{lmi}_i=\min_{j\le i}a_j$,$\text{rmi}_i=\min_{j\ge i}a_j$,$len=n-k+1$。

关键结论:最优解中“最大段”只需考虑三种形态——长度为 $len$ 的中间段、长度 $

  1. 中间段 $[l, r=l+len-1]$:候选 $=S_r-S_{l-1}-\min(\text{lmi}_{l-1},\text{rmi}_{r+1})$;
  2. 前缀最大段 $[1..l]\ (l
  3. 后缀最大段 $[l..n]\ (n-l

三者取最大即答案。$k=2$ 时上面第三类退化($k=2$ 只能前/后缀一刀切),可由 $\max_i|S_n-2S_i|$ 化简为 $O(1)$ 端点公式 $Ans=\max(S_{n-1}-a_n,\ S_n-2a_1)$(前缀和单调 ⇒ 极值只可能在 $i=1$ 或 $i=n-1$ 取到)。

复杂度 $O(n)$,空间 $O(n)$。

分子任务题解(原题)

子任务 限制 分档做法
样例(0 分) — 与题面样例一致。
1(11 分) $n\le15$ 枚举所有 $k-1$ 个切点的组合 $\binom{n-1}{k-1}$,逐个算最大/最小段和取 max,$O(\binom{n-1}{k-1}\cdot n)$。
2(11 分) $k=2$ $O(n)$ 扫一遍 $\max_i S_n-2S_i $(或 $O(1)$ 端点公式)。
3(21 分) $k=3$ 满分“三类枚举”$O(n)$ 可直接解决 $k=3$;也可先枚举“中间段” $O(n)$(配合前后缀 min)或 $O(n^2)$ 保底。
4(15 分) $n\le300$ 朴素 $O(n\cdot len)\le O(n^2)$:枚举每个候选最大段 $[l..r]$,段外最小用 $\text{lmi}/\text{rmi}$ 求。
5(21 分) $n\le3000$ 同 $O(n^2)\approx9\times10^6$ 可过,或直接满分 $O(n)$。
6(21 分) 无限制 满分 $O(n)$ 三类枚举。

易错点

  • $\text{lmi}_0=\text{rmi}_{n+1}=\infty$(否则段外“最小”会取到 0 造成错误);
  • 三类枚举的循环边界:前缀段 $ln-len$、中间段 $l+len-1\le n$;
  • $k=2$ 特判要独立写,避免走通用分支被“中间段”假象误导;
  • 前缀和上限约 $3\times10^{14}$,务必 long long。

题目四 召唤小鸟(bird)

难度与考察点

  • 难度:★★★☆☆(提高;CF 922E,约 2100)。
  • 考察点:背包 DP;状态的“最大化剩余魔力”设计;上限增长 $W+jB$ 的处理;64 位与内存边界。

题意

$n\le10^3$ 棵树依次经过;初始魔力 $W$、上限 $W$;第 $i$ 棵树可召唤 $0..c_i$ 只,每只花费 $cost_i$,召唤后上限 $+B$;走到下一棵树回 $X$ 魔力且不超过上限。求最多召唤数($\sum c_i\le10^4$)。

满分题解(背包 DP)

$j$ 只召唤后上限必为 $W+j\cdot B$,与顺序无关。设 $dp[i][j]=$ 考虑前 $i$ 棵树、共召唤 $j$ 只后剩余魔力的最大值(不可行记 $-1$)。剩余越多越优(只影响后续购买力)。

转移:第 $i$ 棵树召唤 $k\ (0\le k\le\min(c_i,j))$ 只,需 $dp[i-1][j-k]\ge k\cdot cost_i$:

$$ dp[i][j]=\max_{k}\Big(dp[i][j],\ \min\big(dp[i-1][j-k]-k\cdot cost_i+X,\ W+j\cdot B\big)\Big). $$

答案 = 最大的 $j$ 使 $dp[n][j]\ne-1$。

复杂度:设 $C=\sum c_i\le10^4$,总转移约 $O\big(C(C+n)\big)\le1.1\times10^8$ 轻量运算,2 s 内可过;空间 $dp[1010][10010]$(long long,约 81 MB,512 MB 内)。

分子任务题解(原题按测试点分档)

档位 限制 分档做法
测试点 1–2 $n\le20$ 递归/DFS 枚举每棵树召唤只数(可行性剪枝),小规模可过;也可直接跑满分 DP。
测试点 3–4 $W,B,X,cost_i\le10^4$ 数值小,直接实现满分 DP 即可(无需担心 $10^{13}$ 溢出),或按“钱不多”的状态压缩枚举。
测试点 5–10 $n\le10^3$,$8000\le\sum c_i\le10^4$,$cost_i\le10^9$ 满分背包 DP;此档考验 64 位处理:$k\cdot cost_i$ 与 $W+jB$ 均可达 $10^{13}$。

易错点

  • $w+j\cdot b$、$k\cdot cost_i$ 用 long long;
  • 上限截断写 min(..., w+j*b)($B=0$ 上限不增、$X=0$ 不恢复);
  • memset(dp,-1,…) 后只从可行状态转移;答案倒序找第一个非 $-1$ 的 $j$。

题目五 暗号计数(binaria)

难度与考察点

  • 难度:★★★★☆(提高+/省选-;图论建模思维题)。
  • 考察点:相邻窗口差分建模;并查集(或同余类链)合并等值约束;矛盾检测;组合数 $C(x,y)\bmod P$(质数 $10^6+3$);迭代 DFS 防爆栈。

题意

给定 $n,k$ 与全部 $n-k+1$ 个窗口和 $s_i=\sum_{j=i}^{i+k-1}b_j$,求满足条件的 01 串 $b_1..b_n$ 数量 $\bmod10^6+3$。$n\le10^6$,数据保证有解。

满分题解($O(n+k)$)

Step 1 差分定关系。 $$ s_{i+1}=s_i-b_i+b_{i+k}\ \Longrightarrow\ b_{i+k}-b_i=s_{i+1}-s_i=:d_i . $$ $d_i=0$ ⇒ $b_i=b_{i+k}$(**等值**);$d_i=1$ ⇒ $b_i=0,b_{i+k}=1$;$d_i=-1$ ⇒ $b_i=1,b_{i+k}=0$(定值)。

Step 2 并查集/类链。 等值位置并成一个分量(标程用邻接表 + 显式栈迭代 DFS 打连通块标记)。若分量中任一位置被“定值”,整块值确定;块内出现 0/1 冲突则无解(本题数据保证不出现)。

Step 3 统计自由变量。 沿“每步 $-k$”回溯可知每个分量都含一个 $\le k$ 的位置,故自由变量数 = 前 $k$ 位中未被定值的个数 $x$;前 $k$ 位中已定为 1 的有 $p$ 个,则还需 $y=s_1-p$ 个 1,答案 $=\binom{x}{y}\bmod P$(其余位置由差分唯一推出且自动满足所有窗口和)。

Step 4 组合数取模。 $P=10^6+3$ 为质数且 $>n$,预处理 $0..k$ 阶乘与逆元后 $O(1)$ 求 $\binom{x}{y}$。

复杂度 $O(n+k)$,空间 $O(n)$。

分子任务题解(原题)

子任务 限制 分档做法
1(12 分) $n\le10,k\le3$ 暴力枚举 $2^n$ 个串逐一核对窗口和。
2(12 分) $n\le10$ 同上暴力即可($2^{10}=1024$)。
3(16 分) $n\le1000,k\le10$ 枚举前 $k$ 位 $2^k\le1024$ 种赋值,用 $b_{i+k}=b_i+s_{i+1}-s_i$ 递推全串并校验,$O(n2^k)\approx10^6$。
4(16 分) $n\le10^6,k\le20$ $2^k$ 枚举过大:按模 $k$ 同余的“类链”独立做定值/矛盾传播(即满分做法去并查集的形式)$O(n)$;或直接用满分做法。
5(16 分) $n\le10^6,k\le3000$ 必须 $O(n)$:满分做法(并查集或类链扫描)。
6(28 分) 无额外限制 满分 $O(n+k)$:差分建约束 → 迭代 DFS 求分量 → 定值传播 → $x,y$ → 组合数。

易错点

  • 递归会爆栈:$k$ 较小时等值链可达 $O(n/k)$($k=2$ 时约 $5\times10^5$ 层),标程改用显式栈迭代 DFS;
  • 差分方向:$b_{i+k}-b_i=s_{i+1}-s_i$,赋值别写反;
  • 若出现矛盾(块内 0/1 冲突、$y<0$ 或 $y>x$)答案为 $0$——本数据保证不出现,但实现上可留检测;
  • 阶乘只预处理到 $k$($x\le k$);逆元用费马小定理;数组开 $10^6+5$。

附:与交付文件对照

  • 每题的完整官方级题解(推导细节、复杂度、代码走查)见 color|piece|balance|bird|binaria/solutions/editorial.md;
  • 标程见 */src/*.cpp(已通过全部 data/samples/extra 回归与暴力对拍);
  • 大样例在 */samples/extra1|extra2.in/.out。

TOD

avatar wfirstzhang 2026-09-09 08:19:07

AI:

  1. CF1849E code(MS) explain

  2. CDQ

  3. ADHOC/... => NOIP-LESSONS

CSP-S Linux相关知识(详细)

avatar wfirstzhang 2026-09-05 11:01:14

CSP-S 初赛 Linux 知识整理

整理范围:2019–2024 CSP-S 提高级第一轮真题(洛谷题面)+《NOI 2026 基础知识题库》 +《全国青少年信息学奥林匹克系列竞赛大纲(2025 年修订版)》。 结论先行:初赛 Linux 题题量少(每年 0–2 题,单选每题 2 分),但高度固定, 基本就是下面这份文档里的命令和概念换着花样考,背熟即可稳拿分。


0. 考纲定位(2025 年修订版大纲)

大纲把知识点按入门级 / 提高级 / NOI 级划分,高级自动包含低级全部内容, 每个知识点后 【X】 为学习难度系数(1~10)。

与 Linux / 编程环境直接相关的条目只有两处:

入门级 §2.1.1 基础知识与编程环境(难度 1,CSP-J 范围,CSP-S 默认已掌握)

    1. Windows、Linux 等操作系统的基本概念及其常见操作
    1. 使用图形界面新建、复制、删除、移动文件或目录
    1. 使用 Windows 下的集成开发环境(例如 Dev-C++ 等)
    1. 使用 Linux 下的集成开发环境(例如 Code::Blocks 等)
    1. 常用编译命令 g++ 的基本使用
  • 另 §2.1.2:编辑、编译、解释、调试的概念;文件的基本概念、 文本文件与二进制文件类型;文件重定向、文件读写等操作

提高级 §2.2.1 基础知识与编程环境(难度 5,CSP-S 初赛范围——本文重点)

  1. Linux 系统终端中常用的文件与目录操作命令
  2. Linux 系统下常见文本编辑工具的使用
  3. 常用编译命令 g++ 与相关编译选项
  4. 在 Linux 系统终端中运行程序,使用 time 命令查看程序用时
  5. 调试工具 GDB 的使用

也就是说:初赛 Linux 大题范围=第 3、5、6、7 章+第 4 章的 g++/time/gdb, 难度 5,属于"会用即可、不考原理深挖"。NOI 级不再新增环境类知识点。


1. 历年真题中的 Linux 题一览

年份 题目 选项 答案 解析
2019 编译器的功能是() A. 将源程序重新组合 / B. 将一种语言(通常是高级语言)翻译成另一种语言(通常是低级语言)/ C. 将低级语言翻译成高级语言 / D. 将一种编程语言翻译成自然语言 B 编译器=高级语言 → 低级语言(汇编/机器语言)的翻译程序。注意不是"自然语言",也不是"低级→高级"(那是反编译/反汇编)。
2020 (无 Linux 题) — — 当年 15 道单选无操作系统题,但不代表不考。
2021 在 Linux 系统终端中,列出当前目录下所有文件和目录的命令为() ls / cd / cp / all ls ls = list,列目录内容;cd = 切换目录;cp = 复制。all 不是命令。
2022-1 在 Linux 系统终端中,切换工作目录的命令为() ls / cd / cp / all cd cd = change directory。与上题是"姊妹题",出题人就爱在 ls/cd/cp/mkdir/pwd 里轮换。
2022-2 同时用 time 命令和秒表为某程序计时,time 输出 real 0m30.721s user 0m24.579s sys 0m6.123s,最接近秒表计时的是() 30s / 24s / 18s / 6s 30s real = 实际流逝时间(=秒表时间);user = 用户态 CPU 时间;sys = 内核态 CPU 时间。秒表看的是 real。
2023-1 在 Linux 系统终端中,哪个命令可用于创建一个新的目录 newdir / mkdir / create / mkfold mkdir mkdir = make directory。干扰项都是"看起来像"但不存在的命令。
2023-2 哪个命令能把 main.cpp 编译并生成名为 main 的可执行文件 g++ -o main main.cpp / g++ -o main.cpp main / g++ main -o main.cpp / g++ main.cpp -o main.cpp A g++ -o <输出名> <源文件>。-o 后面紧跟的是输出文件名,所以只有 A 把输出定为 main、输入定为 main.cpp。
2024 在 Linux 系统中,如果想显示当前工作目录的路径,应该使用哪个命令 pwd / cd / ls / echo pwd pwd = print working directory。echo 只是原样输出参数(echo hello → hello),不查路径。

命题规律:初赛 Linux 题只考三类——① 5 个基础命令(ls/cd/pwd/mkdir/cp/mv/rm 二选一辨析); ② g++ 编译命令写法;③ time 的 real/user/sys。外加题库里的 vim/gdb/进程题。下面逐个讲透。


2. Linux 基础常识(题库高频)

  • NOI / CSP 复赛使用的操作系统是 Linux(发行版为 NOI Linux,基于 Ubuntu)。初赛考 Linux 就是为复赛上机做准备。
  • Linux 是开源(开放源代码)的操作系统,可自由使用和修改。Windows/macOS 则不是。
  • 最高权限用户是 root(超级用户),可以做任何事;普通选手账户权限受限。
  • 默认使用的 Shell 是 bash(Bourne Again Shell)。"终端"里敲的命令都是由 bash 解释执行的。
  • 文件系统大小写敏感:FILE 和 File 是两个不同文件;目录 FOLDER 和 Folder 也是两个不同目录。 Windows 下它们会被视为同一个——这是初赛最爱考的对比点。
  • Linux 下换行符是 \n(Windows 是 \r\n,Mac 旧系统是 \r)。按行读入数据时多出的 \r 会造成 bug,复赛注意。
  • 同一目录下,文件不能与子目录同名(名字是目录项的唯一标识,同名会冲突)。
  • 虚拟终端切换:Ctrl + Alt + F1~F6 在各终端之间切换;NOI Linux 下 Alt + F2 可快速呼出命令/返回图形环境。 记法:凡是"切换终端"带 Ctrl+Alt+Fn,凡是"NOI Linux 切回图形"认 Alt+F2。

文件系统结构与路径

  • 根目录:整个文件系统的起点,表示为 /,所有文件和目录都从它往下长。
  • 家目录(Home Directory):用户登录后所在的初始目录。普通用户一般在 /home/用户名/ 下, root 超级用户的家目录是 /root。cd ~ 或裸 cd 回到的就是这里。
  • 当前目录用 . 表示,上级目录用 .. 表示(所以 cd .. 是回上一级)。
  • 绝对路径:从根目录 / 开始的完整路径,如 /usr/bin/g++,无论身在何处都指向同一位置。
  • 相对路径:从当前目录开始的路径,如 ./test(当前目录下的 test)、../doc/1.txt(上级目录的 doc/1.txt)。 ./a.out 中的 ./ 就是"当前目录"的意思。

文件权限入门(为 chmod 打底,详见 §3.7)

  • Linux 中每个文件和目录都有三组权限,分别对应三种身份: 用户(User, u)=文件所有者;组(Group, g)=同组用户;其他(Other, o)=其余所有人。
  • 每组权限固定由三个字符表示,顺序永远是读(r)、写(w)、执行(x),没有则写 -。
  • 示例 -rwxr-xr--:首字符 - 表示普通文件(d 则为目录); rwx=用户可读可写可执行;r-x=组可读可执行、不可写;r--=其他人只读。

3. 文件与目录命令(核心中的核心)

3.1 显示当前路径:pwd

  • 全称 print working directory,显示当前工作目录的完整(绝对)路径。
  • 例:pwd → /home/student/noi。
  • 对比:cd 是"走过去",pwd 是"问我在哪",ls 是"看看周围有啥"。

3.2 列出目录内容:ls

用法 含义
ls 列出当前目录下的文件和子目录(不含隐藏文件)
ls -a 列出全部,包括以 . 开头的隐藏文件(题库原题)
ls -l 长格式:显示权限、大小、修改时间等(可查看文件大小,题库原题)
ls /tmp 列出指定目录
  • 隐藏文件:名字以 . 开头,如 .bashrc,正常 ls 看不见,必须 ls -a。

3.3 切换目录:cd

用法 含义(题库原题:返回上一级是 cd ..)
cd dir 进入子目录 dir
cd .. 返回上一级目录
cd . 留在当前目录(一般只用于 ./a.out 这种写法)
cd ~ 回家目录(当前用户的 home)
cd / 去根目录
cd - 回到上一次所在的目录
cd(无参数) 等价于 cd ~
  • 绝对路径以 / 开头(如 /home/student),相对路径不以 / 开头(如 ../tmp)。

3.4 建/删目录:mkdir / rmdir

  • mkdir test:创建名为 test 的新目录(2023 真题)。
  • mkdir -p a/b/c:连父目录一起建(a 不存在也能建出 a/b/c)。
  • rmdir test:删除空目录;非空删不掉,非空要用 rm -r。

3.5 复制 / 改名移动 / 删除:cp / mv / rm

  • cp 源 目标:复制文件。复制目录加 -r:cp -r dir1 dir2。
  • mv <原文件名> <新文件名>:改名(题库原题);mv 文件 目录/:移动。 mv 和 cp 的区别:mv 原位置不留东西(搬家),cp 原位置保留(复印)。
  • rm 文件:删除文件;rm -r test:递归删除 test 目录及其中全部内容(题库原题); rm -f:强制删除不确认。rm 删了不进回收站,rm -r 尤其危险,复赛敲之前看三遍路径。

3.6 查看 / 查找文件

  • cat 文件:连接文件并打印到屏幕(看全文),如 cat hello.cpp。必考!
  • head -n 文件 / tail -n 文件:看开头/结尾 n 行(默认 10 行)。
  • touch a.cpp:新建空文件(或更新时间戳)。
  • find . -name "*.cpp":在当前目录(.)及其子目录中按名查找文件(必考写法)。
  • grep [选项] 模式 [文件]:在文件中按行搜索指定的字符串/模式,命中则整行打印。 全称 Global Regular Expression Print(全局正则打印),名字即功能:全局搜+整行打出。
  • 基本用法:grep "hello" file.txt=打印 file.txt 中所有含 hello 的行; 不写文件名则从标准输入读,正好接管道:ps aux | grep python=从进程列表里过滤出含 python 的行。
  • 常用选项(初赛只认这几个):-n 显示行号(grep -n "hello" f.txt); -i 忽略大小写;-v 反选(打印不含模式的行);-r 递归搜整个目录; -c 只数命中行数(等价于 ... | wc -l)。
  • 组合技(常考):cat file.txt | grep "main" | wc -l=先显示、再过滤含 main 的行、最后数行数。
  • 对比 find:find . -name "*.cpp" 是按文件名找文件,grep "main" *.cpp 是按文件内容找字符串, 二者经常放一起辨析:找文件用 find,找内容用 grep。
  • wc:字数统计。wc -l file 数行数(最常考),wc -w 数单词数,wc -c 数字节数。
  • | 管道、>/>> 重定向见 §7。echo、man 见 §7。
  • 记忆命令全称助记:ls=list、cd=change directory、cp=copy、mv=move、 rm=remove、pwd=print working directory、cat=concatenate。

3.7 权限管理:chmod(重中之重!)

chmod=修改文件/目录的权限模式,是整份文档里考得最多的单个命令,必须掌握两种写法。

1. 数字表示法(绝对模式,最常考!)

权限用三位八进制数表示,分别对应用户(u)、组(g)、其他(o)。先背换算:

  • r(读)= 4,w(写)= 2,x(执行)= 1,-(无)= 0,同组数字相加:
  • 7 = 4+2+1 → rwx;6 = 4+2 → rw-;5 = 4+1 → r-x;4 = 4 → r--。

常见考题:

  • chmod 755 file:用户 7→rwx,组 5→r-x,其他 5→r-x。给可执行程序的典型权限。
  • chmod 644 file:u=rw-(6), g=r--(4), o=r--(4)。给普通文本文件的典型权限。

2. 符号表示法:chmod [谁][操作符][权限] 文件名

  • 谁:u(用户)、g(组)、o(其他)、a(所有人); 操作符:+(添加)、-(移除)、=(设置);权限:r/w/x。
  • 示例:chmod o+x file(给其他人加执行权限); chmod g-w file(移除组的写权限);chmod a=rw file(所有人设为读写)。
  • 对照 §2 的 -rwxr-xr--:看懂 ls -l 首列才能做符号法——ls -l+chmod 经常绑在一起考。

4. 编译与运行(2023 真题+题库 5 题)

4.1 基本编译命令

g++ -o main main.cpp        # main.cpp → 可执行文件 main(真题答案写法)
g++ main.cpp -o main        # 等价,-o 后面永远跟"输出名"
./main                      # 运行当前目录下的 main(注意前面的 ./)
  • 当前目录不在 PATH 里,所以运行当前目录程序必须加 ./: 当前目录下编译好的 a.out,执行命令是 ./a.out(题库原题),直接敲 a.out 会报找不到命令。
  • -o = output,指定输出文件名;不写 -o 时默认输出 a.out。 2023 年真题 4 个选项就是考"你认不认得出 -o 后面跟的是输出"。

4.2 常用编译选项(题库全覆盖)

选项 含义 例子
-o 文件名 指定输出文件名 g++ -o main main.cpp
-c 只编译不链接,生成 .o 目标文件 g++ -c main.cpp → main.o
-g 生成调试信息,供 gdb 调试用 g++ -g -o main main.cpp
-Wall 打开全部警告信息(Wall = Warn all) g++ -Wall -o main main.cpp
-O2 二级优化(复赛评测机一般开 -O2) g++ -O2 -o main main.cpp
-std=c++17 指定 C++ 标准 g++ -std=c++17 -o main main.cpp
-lm 链接数学库(用了 math.h 的函数时加) g++ -o main main.cpp -lm
  • 口诀:o 定名、c 暂存(.o)、g 调试、Wall 警告、O2 优化、lm 数学。
  • 复赛标准编译命令一般形如:g++ -o foo foo.cpp -lm -O2 -std=c++17 -Wall, 初赛把每个开关拆开单考,含义背熟即可。
  • 补充写法:g++ hello.cpp(无 -o)默认生成 a.out(Linux 下无 .exe 后缀); g++ -O2 -std=c++11 hello.cpp 即"开 O2 优化、按 C++11 标准编译"的连写,选项可叠加、顺序无关。

4.3 编译器概念(2019 真题)

  • 编译器的功能:把一种语言(通常是高级语言)翻译成另一种语言(通常是低级语言,如汇编/机器语言)。
  • 反向(低级→高级)叫反编译;翻译成自然语言(中文/英文)不是编译器干的事——排除法秒选 B。

5. 调试、进程与计时

5.1 gdb 调试器

  • NOI Linux 下调试程序用 gdb(题库原题)。
  • 最小流程:g++ -g -o main main.cpp(必须先 -g)→ gdb ./main → 常用子命令 run(运行)、break 行号(断点)、next(单步)、print 变量(看值)、quit(退出)。

5.2 看进程 / 杀进程

命令 含义(均为题库原题)
ps 查看当前系统中的进程
ps aux / top 看所有进程及 CPU/内存占用(题库"查看 CPU 占用"认 ps)
kill <pid> 按进程号终止进程,如 kill 1234
killall test 按名字杀死所有名为 test 的后台进程
Ctrl + C 中断终端中正在运行的前台程序(死循环救命键)
  • Ctrl + C = 中断,Ctrl + Z = 挂起,Ctrl + D = 输入结束(EOF)。

5.3 time 计时(2022 真题详解)

real 0m30.721s    ← 墙上时钟走过的时间 = 秒表时间
user 0m24.579s    ← 程序在用户态消耗的 CPU 时间
sys  0m6.123s     ← 程序在内核态消耗的 CPU 时间
  • 秒表对标 real,所以 30.721s → 选 30s。
  • 单核单线程:user + sys ≈ CPU 总耗时 ≤ real(多出的几秒是等待 I/O)。
  • 多核/多线程:user 可以大于 real(多个核同时烧 CPU)。

6. vim 编辑器(题库 4 题,年年有)

vim 有两种大状态:命令模式(默认,敲命令)按 Esc 回到;插入模式(打字)按 i/a/o 进入。

输入(命令模式下) 效果
:q! 强制退出、不保存(题库原题)
:wq / :x 保存并退出(题库原题二选一认 :wq)
:w 只保存不退出
:12 跳到第 12 行(题库原题)
/12 在文件中查找字符串 12(题库原题),n 下一个,N 上一个
i / a / o 进入插入模式(光标处/后一格/下一行新起一行)
Esc 回命令模式
dd 删除当前行;yy 复制当前行;p 粘贴
u 撤销;gg 文件头;G 文件尾;:set nu 显示行号
  • 记忆钩:q 退出、w 保存、! 强制、数字跳行、斜杠查找。考题就是 :q!/:wq/:12//12 四换一。

7. Shell 常用符号(重定向 / 管道)

符号 含义 例子
> 重定向输出(覆盖文件) ./main > out.txt
>> 重定向输出(追加到文件末尾) echo hi >> log.txt
< 重定向输入(从文件读) ./main < in.txt
`\ ` 管道(前一个命令的输出当后一个的输入) `ls \ grep cpp`
; 顺序执行多条命令 g++ -o main main.cpp; ./main
&& 前一条成功才执行后一条 g++ -o main main.cpp && ./main
* / ? 通配符(任意多 / 单个字符) rm *.o
~ 家目录 cd ~
echo 原样输出参数(2024 真题干扰项) echo hello → hello
man 命令 看手册 man g++
  • 复赛典型用法:./main < data.in > data.out 一行搞定输入输出重定向。

8. 易错速记表(一页背完)

  1. NOI 考试 OS=Linux;最高权限用户=root;默认 Shell=bash。
  2. 列目录 ls,看隐藏 ls -a,看大小 ls -l;在哪 pwd;换目录 cd,上级 cd ..;建目录 mkdir。
  3. 改名/移动 mv,复制 cp(目录加 -r),删目录 rm -r test;rm 删了极难恢复。
  4. 编译 g++ -o main main.cpp(-o 后跟输出,默认 a.out);运行 ./a.out(必须 ./)。
  5. -c 生成 .o,-g 调试信息,-Wall 警告,-lm 数学库,-O2 优化。
  6. 调试 gdb(先 -g 编译);看进程 ps;按名杀 killall test;按号杀 kill pid;死循环 Ctrl+C。
  7. 计时认 real(秒表时间);user 用户态、sys 内核态。
  8. vim::q! 不存退,:wq 存退,:12 跳行,/12 查找。
  9. 大小写敏感:FILE ≠ File;换行 \n;终端切换 Ctrl+Alt+Fn,NOI 回图形 Alt+F2。
  10. 编译器=高级语言→低级语言;权限 chmod 必考:755=可执行程序,644=普通文本,r=4/w=2/x=1。

9. 自测 10 题(含答案)

  1. 列出当前目录(含隐藏文件)用? → ls -a
  2. 返回上一级目录用? → cd ..
  3. 删除当前目录下 test 目录用? → rm -r test
  4. 把 a.cpp 改名为 b.cpp 用? → mv a.cpp b.cpp
  5. 显示当前路径用? → pwd
  6. g++ -o main main.cpp 中 -o main 的意思是? → 指定输出文件名为 main
  7. 只编译不链接生成 .o 的选项是? → -c;带调试信息的选项是? → -g
  8. time 输出 real/user/sys,哪个最接近秒表? → real
  9. vim 不保存强制退出 / 跳到第 5 行 / 查找 abc 分别是? → :q! / :5 / /abc
  10. killall test 和 kill 1234 的区别是? → 前者按名字杀所有 test 进程,后者按进程号杀指定进程

10. 备考建议

  1. 重点攻克 chmod:熟练掌握数字表示法(755、644),怎么换算、典型值对应什么场景。
  2. 理解管道和重定向:明白 |、>、>> 的作用和区别,能读懂 cat file | grep main | wc -l 这类组合。
  3. 动手实践:条件允许就在本机装 Linux 虚拟机(VirtualBox + Ubuntu)或用 WSL(Windows Subsystem for Linux), 把本文命令亲手敲一遍。光靠死记硬背容易混淆,上机一遍就记牢。
  4. 刷真题:把 §1 的历年题遮住答案重做一遍,再拿题库 vim/gdb/ps/-o/-c/-g 题自测。

CSP-S Linux相关知识(From WF_wxl,注意可能不全)

avatar wfirstzhang 2026-09-05 10:21:26

一、Linux 基本概念

  1. 文件系统结构:

    • 根目录:整个文件系统的起点,表示为 /。
    • 家目录(Home Directory):用户登录后所在的初始目录。普通用户的家目录通常在 /home/用户名/ 下,root超级用户的家目录是 /root。
    • 当前目录:用户当前所在的工作目录,用 . 表示。
    • 上级目录:当前目录的上一级目录,用 .. 表示。
  2. 路径:

    • 绝对路径:从根目录 / 开始的完整路径,如 /usr/bin/g++。
    • 相对路径:从当前目录开始的路径,如 ./test 或 ../doc/1.txt。
  3. 文件权限: Linux系统中,每个文件和目录都有三组权限,分别对应三种身份:

    • 用户(User, u):文件的所有者。
    • 组(Group, g):文件所属用户组的成员。
    • 其他(Other, o):既不是所有者,也不是组成员的其他人。 每组权限都由三个字符表示,顺序永远是 读(r)、写(w)、执行(x)。如果没有某个权限,则用 - 表示。 示例:-rwxr-xr--
    • 第一个字符 - 表示这是一个普通文件(如果是 d 则表示目录)。
    • rwx:用户(u)有读、写、执行权限。
    • r-x:组(g)有读、执行权限,无写权限。
    • r--:其他(o)只有读权限。

二、必考命令详解

1. 文件和目录操作(最核心)

  • ls - 列出目录内容

    • ls:列出当前目录下的文件和目录(不包含隐藏文件)。
    • ls -l:以长格式显示,可以看到详细信息(权限、所有者、大小、修改时间等)。必考!
    • ls -a:显示所有文件,包括隐藏文件(以 . 开头的文件)。
  • cd - 切换当前工作目录

    • cd /home:切换到绝对路径 /home。
    • cd ..:切换到上级目录。
    • cd ~ 或 cd:切换回当前用户的家目录。
    • cd -:切换回上一次所在的目录。
  • pwd - 打印当前工作目录的绝对路径。

  • mkdir - 创建目录

    • mkdir dirname:创建名为 dirname 的目录。
    • mkdir -p parent/child:一次性创建多级目录(如果 parent 不存在也会被创建)。
  • rm - 删除文件或目录

    • rm file.txt:删除文件 file.txt。删除后极难恢复!
    • rm -r dirname:递归删除目录 dirname 及其内部所有内容。非常危险!
    • rm -f file:强制删除,不提示确认。
  • cp - 复制文件或目录

    • cp source.txt dest.txt:将 source.txt 复制为 dest.txt。
    • cp -r sourcedir destdir:递归复制整个目录。
  • mv - 移动文件或目录(也可用于重命名)

    • mv old.txt new.txt:将 old.txt 重命名为 new.txt。
    • mv file.txt /tmp/:将 file.txt 移动到 /tmp/ 目录下。
  • cat - 连接文件并打印到标准输出设备上(用于查看文件内容)

    • cat hello.cpp:在屏幕上显示 hello.cpp 的全部内容。
  • find - 查找文件

    • find . -name "*.cpp":在当前目录(.)及其子目录中查找所有后缀为 .cpp 的文件。

2. 权限管理命令(重中之重!)

  • chmod - 修改文件或目录的权限模式。这是考得最多的命令,必须掌握两种用法。

    1. 数字表示法(绝对模式):最常考! 权限用三位八进制数表示,分别对应 用户(u)、组(g)、其他(o)。

      • r (读) = 4
      • w (写) = 2
      • x (执行) = 1
      • - (无权限) = 0 将一组权限的数字相加即可。
      • 7 = 4+2+1:读、写、执行(rwx)
      • 6 = 4+2:读、写(rw-)
      • 5 = 4+1:读、执行(r-x)
      • 4 = 4:只读(r--)

      常见考题:

      • chmod 755 file:
        • 用户(u):7 -> rwx
        • 组(g):5 -> r-x
        • 其他(o):5 -> r-x
        • 这是给可执行程序的典型权限。
      • chmod 644 file:
        • u=rw- (6), g=r-- (4), o=r-- (4)
        • 这是给普通文本文件的典型权限。
    2. 符号表示法: chmod [谁][操作符][权限] 文件名

      • 谁:u(用户), g(组), o(其他), a(所有人)
      • 操作符:+(添加), -(移除), =(设置)
      • 权限:r, w, x
      • 示例:
        • chmod o+x file:给其他人增加执行权限。
        • chmod g-w file:移除组的写权限。
        • chmod a=rw file:给所有人设置为读写权限。

3. 文本和输出处理命令

  • grep - 在文件中搜索指定的字符串模式。

    • grep "hello" file.txt:在 file.txt 中查找包含 "hello" 的行。
    • grep -n "hello" file.txt:显示匹配行的行号。
    • ps -aux | grep "python":(常考管道|)将 ps 命令的输出传递给 grep,过滤出包含 "python" 的行。
  • wc - 字数统计

    • wc -l file.txt:统计文件行数。
    • wc -w file.txt:统计单词数。
    • wc -c file.txt:统计字节数。
  • | - 管道:将一个命令的输出作为另一个命令的输入。

    • 示例:cat file.txt | grep "main" | wc -l:这个命令组合的意思是:先显示file.txt的内容,然后从内容中搜索包含"main"的行,最后统计这些行的行数。
  • > 和 >> - 输出重定向

    • command > file:将命令的输出重定向到 file 中(覆盖原有内容)。
    • command >> file:将命令的输出追加到 file 的末尾。

三、编译和运行C++程序

这是参加CSP-S必须掌握的实践技能,笔试中会考察基本命令和参数。

  • g++ - GNU C++ 编译器

    • g++ hello.cpp:编译 hello.cpp,默认生成一个名为 a.out 的可执行文件(在Linux上无后缀)。
    • g++ -o hello hello.cpp:编译 hello.cpp,并指定输出的可执行文件名为 hello。-o 参数非常重要且常考!
    • g++ -c hello.cpp:只编译不链接,生成目标文件 hello.o。
    • g++ -O2 -std=c++11 hello.cpp:使用O2优化,遵循C++11标准进行编译。
  • 运行程序

    • ./a.out:运行当前目录下的 a.out 程序。注意:前面的 ./ 是必须的,它告诉系统从当前目录查找程序。

四、备考建议与总结

  1. 记忆命令全称:理解命令英文全称有助于记忆(如 ls=list, cd=change directory, cp=copy)。
  2. 重点攻克 chmod:务必熟练掌握数字表示法(755,644等),10次考试9次都会考到。
  3. 理解管道和重定向:明白 |, >, >> 的作用和区别。
  4. 动手实践:如果条件允许,可以在自己的电脑上安装一个Linux虚拟机(如VirtualBox + Ubuntu)或者使用WSL(Windows Subsystem for Linux),亲手敲一遍这些命令,印象会非常深刻。光靠死记硬背容易混淆。
  5. 刷真题:找近几年的CSP-J/S第一轮真题来做,看看Linux题是怎么出的,考点分布在哪里。

2026 CSP-S初赛模拟题目

avatar wfirstzhang 2026-09-04 21:12:10

提高组 CSP-S 2026 初赛模拟卷 3 —— 原题 + 解析

按目录截图字典序(91–99 页)整理。共 20 道:单选 15 + 阅读程序 3 + 完善程序 2(一道程序算一道)。


一、单项选择(共 15 题,每题 2 分,共 30 分)

1.

今有一空栈 S,对依序进栈的数据元素序列 1,2,3,4,5,…进行以“进栈、进栈、出栈、进栈、进栈、进栈、出栈”为一轮的操作,一直操作下去,那么在 2019 轮操作后,S 的栈顶元素为( )。 A. 10090 B. 10094 C. 10100 D. 10086

答案:B 解:一轮 push 5 个、pop 2 个,净增 3 个,消耗编号 5 个。栈顶是本轮第 4 个数,即 5k-1。k=2019 时 5×2019-1=10094。 注意点:别数错每轮 pop 掉的是刚 push 的;栈顶不是 5k。

2.

对有序数组 {5,13,19,21,37,56,64,75,88,92,100} 进行二分查找,等概率的情况下,查找成功的平均查找长度(平均比较次数)是( )。 A. 35/11 B. 34/11 C. 3 D. 34/10

答案:C 解:11 个数,mid 取下整。判定树 1/2/4/4 层分布,比较总数 1+4+12+16=33,33/11=3。 注意点:成功 ASL 分母是 11;失败才数空指针。

3.

以下选项中各有三个数,其中第一个为八进制数,第二个为十进制数,第三个为十六进制数。以下选项中三个数相同的为( )。 A. 120 82 50 B. 144 100 68 C. 300 200 C8 D. 1762 1010 3F2

答案:D 解:1762_8=512+448+48+2=1010,3F2_16=768+240+2=1010。 注意点:第一个是八进制,第三个是十六进制,F=15,按权展开逐个验算。

4.

将 {2,6,10,17} 分别存到某个地址区间为 0~10 的哈希表中,如哈希函数 h(x)=( ),将不会产生冲突。 A. x mod 11 B. x² mod 11 C. 2x mod 11 D. [√x] mod 11

答案:D 解:A:17%11=6 撞 6;B:6²,17² 都余 3;C:2×6,2×17 都余 1;D 得 1,2,3,4 互异。 注意点:[] 是取整;4 个数逐个代入验算。

5.

二分图是指能将顶点分成两个部分,每一部分的顶点内部没有边相连的简单无向图,那么有 12 顶点的二分图至多有( )条边。 A. 18 B. 24 C. 36 D. 66

答案:C 解:K_{a,b},a+b=12,边 a×b≤6×6=36。 注意点:别和完全图 12×11/2=66 混。

6.

有一个含有 k 个不同的数的数集 S={x₁,x₂,…,xₖ},若在 S 中有一个数 xᵢ(1xᵢ₊₁>…>xₖ,则称 xᵢ 为 S 的“峰顶”。S 称为单峰的。下面有几行伪码,请将 a~e 五处代码填入算法之中,使得算法能正确找到 S 的峰顶。

a. S[mid]<S[mid+1]
b. S[mid]>S[mid+1]
c. Search(1,mid-1)
d. Search(mid+1,k)
e. return S[mid]

Search(1,k){
  mid=k/2;
  if (S[mid]>S[mid-1] && ___) { ___; }
  else if (S[mid]>S[mid-1] && ___) { ___; }
  else ___;
}

正确的填空顺序是( )。 A. a,c,b,d,e B. a,d,b,c,e C. b,e,a,d,c D. b,e,a,c,d

答案:C 解:先判峰顶 S[mid]>S[mid-1]&&b 则 e;上升段 S[mid]>S[mid-1]&&a 则往右 d;否则往左 c。 注意点:峰顶条件是同时大于两侧,这是唯一 return;截图 C/D 印刷重影,以逻辑为准。

7.

如果不在快速排序中引入随机化,可能导致的结果是( )。 A. 数据访问越界 B. 陷入死循环 C. 将排序结果错误 D. 排序时间退化为平方级

答案:D 解:有序数据 + 取端点 pivot 即退化;只影响性能不影响正确性。 注意点:不会死循环 / 越界 / 结果错误。

8.

二进制数 -1101010 的补码是( )。 A. 0010101 B. 10010110 C. 10010101 D. 01101010

答案:B 解:幅值 7 位,8 位原码 11101010 → 反码 10010101 → 补码 10010110。 注意点:C 是反码,别选错;注意 8 位符号位。

9.

设二进制数 x 的值是 11001101,若想通过 x&y 运算使 x 中的低 4 位不变,高 4 位清零,则 y 的二进制数是( )。 A. 11100000 B. 00001100 C. 00001111 D. 00000010

答案:C 解:&1 保留,&0 清零。要低 4 保留、高 4 清零。 注意点:别和 | 混。

10.

循环链表的主要优点是( )。 A. 不需要头指针 B. 已知某个结点的位置后,能很容易找到它的直接前驱结点 C. 在进行删除操作后,能保证链表不断开 D. 从表中任一结点出发都能扫描整个链表

答案:D 解:B 是双向链表优点;A/C 不对。 注意点:关键词“循环”→ 回路可遍历。

11.

若某算法的计算时间表示为递推关系式 T(N)=N+T(N/2),则该算法的时间复杂度为( )。 A. O(N²) B. O(N log N) C. O(N) D. O(1)

答案:C 解:N+N/2+N/4+…=2N。 注意点:Master a=1,b=2 情形。

12.

若某算法的计算时间表示为递推关系式 T(N)=3T(N/4)+n log₂n,则该算法的时间复杂度为( )。 A. O(n) B. O(n log₂n) C. O(n log₂²n) D. O(n²)

答案:B 解:n^{log₄3}≈n^0.79 < n log n,Master Case 3,正则条件成立。 注意点:别看到 3T 就选 log²。

13.

由 5 个不同的结点构成的无根树有( )种。 A. 3125 B. 125 C. 32 D. 1024

答案:B 解:Cayley n^{n-2}=5³=125。 注意点:结点不同 = 标号树;无标号才 3 种。

14.

2019 年 10 月 14 日是星期一,1978 年 10 月 14 日是( )。 A. 星期日 B. 星期五 C. 星期一 D. 星期六

答案:D 解:1978-10-14 到 2019-10-14 共 41 年 + 10 个闰日(含 2000),14975%7=2,周一倒推 2 天即周六。 注意点:2000 是闰年;1980–2016 共 10 个闰日。

15.

有 9 个顶点的简单无向连通图最多有( )条边。 A. 40 B. 81 C. 72 D. 36

答案:D 解:9×8/2=36,连通不增加上限。 注意点:A40/B81/C72 是定向 / 自环陷阱。


二、阅读程序(程序输入不超过数组或字符串定义的范围;判断题正确填√,错误填 X;特殊说明外,判断题每题 2 分,选择题每题 3 分,共 40 分)

阅读 (1)

1  #include <iostream>
2  using namespace std;
3  int n,m,i,j,a,b,head,tail,ans;
4  int graph[100][100];
5  int degree[100];
6  int len[100];
7  int queue[100];
8  int main() {
9    cin >> n >> m;
10   for (i=0; i<n; i++)
11     for (j=0; j<n; j++) graph[i][j] = 0;
12   for (i=0; i<n; i++) degree[i] = 0;
13   for (i=0; i<m; i++) {
14     cin >> a >> b;
15     graph[a][b]=1;
16     degree[b]++;
17   }
18   tail = 0;
19   for (i=0; i<n; i++)
20     if (!degree[i]) {
21       queue[tail] = i;
22       tail++;
23     }
24   head=0;
25   while (tail < n) {
26     for (i=0; i<n; i++)
27       if (graph[queue[head]][i] == 1) {
28         degree[i]--;
29         if (degree[i] == 0) {
30           queue[tail] = i;
31           tail++;
32         }
33       }
34     ++head;
35   }
36   ans = 0;
37   for (i=0; i<n; i++) {
38     a = queue[i];
39     len[a] = 1;
40     for (j=0; j<n; j++)
41       if (graph[j][a] == 1 && len[j] + 1 > len[a])
42         len[a] = len[j] + 1;
43     if (len[a] > ans) ans = len[a];
44   }
45   cout << ans << endl;
46   return 0;
47 }

■判断题

  1. 输入时将 degree[b]++ 改为 degree[a]++,则运行结果不变。 ( )
  2. 该程序的时间复杂度是 O(n)。 ( )
  3. 删除主函数开头对 graph 和 degree 执行清零操作的代码,程序输出不变。 ( )
  4. 若有以下数据,则输出为 3。 ( )
    4 5
    0 2
    1 3
    0 1
    0 3
    2 3

■选择题

  1. graph 存储的是( )。 A. 用邻接矩阵存储的图结构 B. 每个结点的入度 C. 拓扑排序结果 D. 以各结点为终点的最长路径长度
  2. 这个程序用了( )。 A. 基数排序 B. 归并排序 C. 拓扑排序 D. 堆排序

答案:16.X 17.X 18.√ 19.√ 20.A 21.C 解:邻接矩阵存 DAG,先 Kahn 求拓扑序 queue,再 len[a]=max(len[前驱]+1),ans 为最长点数。16 改成 degree[a]++ 后入度全错;17 两重 n 循环是 O(n²+m);18 全局数组默认 0;19 边为 0→2,1→3,0→1,0→3,2→3,最长 0-1-3 长 3。 注意点:第 41 行 graph[j][a] 用结点号当下标,仅当拓扑序恰为数字序时 coincidental 正确,本卷数据满足。

阅读 (2)

1  #include <iostream>
2  #include <string>
3  #define LL long long
4  using namespace std;
5  LL l, r, f[12][10][10][2][2][2], a[20];
6  LL Dfs(LL now, LL p, LL pp, LL _4, LL _8, LL top, LL hw) {
7    if (_4 && _8) return 0;
8    if (!now) return hw;
9    if (!top && f[now][p][pp][_4][_8][hw] != -1)
10     return f[now][p][pp][_4][_8][hw];
11   LL Up = top ? a[now] : 9;
12   LL ret(0);
13   for (LL i=0; i<=Up; ++i)
14     ret += Dfs(now-1, i, p, _4||(i==4), _8||(i==8),
15                top && (i == Up), hw||(pp==p && p==i));
16   if (!top) f[now][p][pp][_4][_8][hw] = ret;
17   return ret;
18 }
19
20 inline LL Solve(LL x) {
21   LL tot(0);
22   while (x) {
23     a[++tot] = x % 10;
24     x /= 10;
25   }
26   if (tot != 11) return 0;
27   LL ret(0);
28   for (LL i=1; i<=a[tot]; ++i)
29     ret += Dfs(tot-1, i, 0, (i==4), (i==8), i==a[tot], 0);
30   return ret;
31 }
32
33 int main() {
34   cin >> l >> r;
35   memset(f, -1, sizeof(f));
36   cout << Solve(r) - Solve(l-1) << endl;
37   return 0;
38 }

■判断题

  1. 同时包含 4 和 8 的数都不可能被统计。 ( )
  2. 相邻数位中,超过 3 个位相同的数都不可能被统计。 ( )

■选择题

  1. 下列选项中,( )是合法(可能被统计)的数。 A. 2323234823 B. 1015400080 C. 23333333333 D. 10010012022
  2. 当输入 12121284000 12121285550 时,程序输出结果为( )。 A. 5 B. 457 C. 455 D. 6

答案:22.√ 23.X 24.C 25.A 解:数位 DP,统计 11 位数中“不同时出现 4 和 8、且含连续 3 位相同”的个数。Dfs(now,p,pp,_4,_8,top,hw):_4&&_8 剪 0,!now 返回 hw,hw|=(pp==p&&p==i) 检连续 3 同。Solve(x) 要求 tot==11,答案 Solve(r)-Solve(l-1)。22 即第 7 行;23 中 4 连同必然含 3 连同,反而能被统计,“都不能”错。24:A/B 只有 10 位(tot!=11 return 0)且都同时含 4、8;D 无 3 连同(hw=0);只有 C 11 位、无 4/8、有 3 连同。25:区间只有 1551 个数,前缀 1212128 固定且已有 8,故后缀禁 4,千位只能取 5;555* 只有 85550 一个(再大超上限);85XXX 型后三位同只有 5000,5111,5222,5333 四个(5444 含 4 排除,5555 超上限),共 5 个。 注意点:hw 是“有 3 连”才计数,别理解反;tot!=11 直接 0 是大坑;前导已有 8 则后边禁 4。

阅读 (3)

1  #include <iostream>
2  using namespace std;
3  int n, i, j, x, y, nx, ny;
4  int a[40][40];
5  int main() {
6    for (i=0; i<40; i++)
7      for (j=0; j<40; j++) a[i][j] = 0;
8    cin >> n;
9    y = 0;
10   x = n - 1;
11   n = 2 * n - 1;
12   for (i=1; i<=n*n; i++) {
13     a[y][x] = i;
14     ny = (y - 1 + n) % n;
15     nx = (x + 1) % n;
16     if ((y == 0 && x == n-1) || a[ny][nx] != 0)
17       y = y + 1;
18     else {
19       y = ny; x = nx;
20     }
21   }
22   for (j=0; j<n; j++) cout << a[0][j] << " ";
23   cout << endl;
24   return 0;
25 }

■判断题

  1. 输入 n 时,输出的数字个数为 2n。 ( )
  2. 输入 1 时,结果为 1。 ( )

■选择题

  1. 若输入 3,则输出是( )。 A. 17 24 1 8 15 B. 1 8 24 17 15 C. 15 24 17 8 1 D. 24 17 8 1 15
  2. 若输入 5,则输出的个数是( )。 A. 6 B. 9 C. 8 D. 5
  3. 若输入 4,则输出结果的和是( )。 A. 175 B. 200 C. 150 D. 120
  4. 若输入 2,则输出是( )。 A. 8 6 1 B. 8 1 6 C. 6 1 8 D. 1 8 6

答案:26.X 27.√ 28.A 29.B 30.A 31.B 解:奇数幻方 Siamese:输入 n0 变 n=2*n0-1,从顶行中列开始右上走,撞墙或被占则下移;最后只输出 a[0][*]。26 输出 2n0-1 个;27 输入 1 → n=1 输出 1;28 输入 3 → 5 阶首行 17 24 1 8 15;29 输入 5 → 9 阶 → 9 个数;30 输入 4 → 7 阶,幻和 7×(49+1)/2=175;31 输入 2 → 3 阶首行 8 1 6。 注意点:x=n-1 用的是旧 n,恰为新 n 的中列;程序只打首行,别当整个方阵数。


三、完善程序(单选题,每小题 3 分,共计 30 分)

完善 (1) 循环赛日程表

设有 N 名选手进行循环比赛,其中 N=2ᵐ,要求每名选手与其他 N-1 名选手各赛一次,每名选手每天比赛一次,循环赛共进行 N-1 天,每天都没有选手轮空。输入一个正整数 M,输出表格形式的比赛安排表,一行各数据间用一个空格隔开。

样例输入:3 样例输出:

1 2 3 4 5 6 7 8
2 1 4 3 6 5 8 7
3 4 1 2 7 8 5 6
4 3 2 1 8 7 6 5
5 6 7 8 1 2 3 4
6 5 8 7 2 1 4 3
7 8 5 6 3 4 1 2
8 7 6 5 4 3 2 1

其中 0 行表示队伍编号,第 1~N-1 行表示各轮对手的编号。

1  #include <cstdio>
2  using namespace std;
3  const int MAXN = 1026;
4  int a[MAXN][MAXN], m;
5
6  int main() {
7    scanf("%d", &m);
8    int n = 1 << m, k = 1, half = 1;
9    ①;
10   while (k <= m) {
11     for (int i=0; i<half; i++) {
12       for (int j=0; j<half; j++) {
13         a[i][②] = ③;
14       }
15     }
16     for (int i=0; i<half; i++) {
17       for (int j=0; j<half; j++) {
18         a[i+half][j] = ④;
19         a[i+half][j+half] = a[i][j];
20       }
21     }
22     ⑤;
23     k++;
24   }
25   for (int i=0; i<n; i++) {
26     for (int j=0; j<n; j++) {
27       printf("%d", a[i][j]);
28     putchar('\n');
29   }
30   return 0;
31 }
  1. ①处应填( )。 A. a[0][1] = 1 B. a[0][0] = 1 C. a[1][0] = 1 D. a[1][1] = 1
  2. ②处应填( )。 A. 1 B. i+half C. j D. j+half
  3. ③处应填( )。 A. a[i][j] - half B. a[i][j+1] - half C. a[i][j] + half D. a[i1][j1] + half
  4. ④处应填( )。 A. a[i][j+half] B. a[i+half][j+half] C. a[i+half][j] D. a[i][j]
  5. ⑤处应填( )。 A. half /= 2 B. half *= 2 C. half += 2 D. half -= 2

答案:32.B 33.D 34.C 35.A 36.B 解:分治复制:右上 = 左上 + half,左下 = 右上,右下 = 左上,half 翻倍。 注意点:样例首行是队号;half 每轮翻倍。

完善 (2) 银河英雄传说(带权并查集)

阪杨 cok 某因贝特率领的帝国军与杨威利的自由行星同盟军在某星系开战。开战前,每一艘战舰都编了号。这次决战中,杨威利每次发布合并指令,将某两部分舰队集中在某星系上,实施密集攻击。合并指令为 M i j,含义为第 i 号战舰所在的整个舰队,作为一个整体(头在前尾在后)接至第 j 号战舰所在的整个舰队的尾部。询问指令为 C i j,含义是询问第 i 号战舰与第 j 号战舰当前是否在同一列,如果在同一列,那么它们之间有多少艘战舰。

1  #include <bits/stdc++.h>
2  using namespace std;
3  int fa[30001], front[30001], num[30001], x, y, i, n, j, m, T, ans;
4  char ins;
5  int find(int n) {
6    if (fa[n] == n) return fa[n];
7    int fn = find(fa[n]);
8    ①;
9    return fa[n] = fn;
10 }
11
12 int main() {
13   cin >> T;
14   for (i=1; i<=30000; ++i) {
15     fa[i] = i;
16     ②;
17     num[i] = 1;
18   }
19   while (T--) {
20     cin >> ins >> x >> y;
21     int fx = find(x), fy = find(y);
22     if (ins == 'M') {
23       front[fx] += num[fy];
24       fa[fx] = fy;
25       ③;
26       ④;
27     }
28     if (ins == 'C') {
29       if (⑤)
30         cout << "-1" << endl;
31       else
32         cout << ⑥ << endl;
33     }
34   }
35   return 0;
36 }
  1. ①处应填( )。 A. front[n] += front[fa[n]] B. front[n] += front[n] C. front[n] += front[fa[n]](印刷重影,以逻辑为准) D. front[fa[n]] += front[fa[n]]
  2. ②处应填( )。 A. front[i] = 1 B. front[i] = 0 C. front[i] = i D. front[fa[i]] = 0
  3. ③处应填( )。 A. num[fx] += num[fy] B. num[fy] += num[fy] C. num[fx] += num[fx] D. num[fy] += num[fx]
  4. ④处应填( )。 A. fx > fy B. fx == fy C. fx < fy D. fx != fy
  5. ⑤⑥处应填( )。 A. abs(front[x]-front[y])-1 B. abs(front[x]-front[y])+1 C. abs(front[num[x]]-front[num[y]])-1 D. abs(front[num[x]]-front[num[y]])+1

答案:37.A 38.B 39.D 40.D 41.A(⑤为 fx!=fy,⑥为 abs(front[x]-front[y])-1) 解:front 为到队首距离,num 为队大小。回溯时 front[n]+=front[fa[n]];初值 0;M 是 fx 挂到 fy,先加 num[fy],即 num[fy]+=num[fx];C 查询要 find 后比 front 差,异根 -1,同根差值 -1。 注意点:M 是 fx 挂到 fy,别加反;C 查询要先 find 再比 front。

题目输入输出文件名称记录

avatar wfirstzhang 2026-09-04 08:29:35

你这套是天空/星象主题,我按同风格给你续上:

最搭的10个 (推荐优先用):

  • eclipse, halo, nova, zenith, umbra, twilight
  • lyra, orion, vega, sirius
  • cumulus, stratus, nimbus — 和 cirrus 正好凑云三件套
  • cosmos, aster, stella, luna, comet, meteor, pulsar, quasar

    再往星座神话续:andromeda, cassiopeia, cygnus, perseus, aquila, phoenix, rigel, altair

    小提醒:aurora borealis 本来就是一个词“北极光”,拆成两题容易混,下次最好只留一个。

.note

avatar wfirstzhang 2026-08-30 12:28:47

题解:灯带问题

问题分析

对于电压 $x$,第 $i$ 个灯亮起当且仅当 $a_i < x$。发光段是极长的连续亮灯区间。

设 $f(x)$ 为电压 $x$ 时的发光段数量。我们需要支持:

  • 修改某个 $a_p$
  • 查询最小的 $x$ 使得 $f(x) \geq k$

    关键观察

    $f(x)$ 仅在 $x = a_i + 1$ 处发生变化(灯 $i$ 恰好在此时亮起)。

    考虑 $f(x)$ 的表达式: $$f(x) = [a_1 < x] + \sum_{i=2}^{N} [a_i < x \leq a_{i-1}]$$

    其中 $[P]$ 在 $P$ 为真时取值 $1$,否则 $0$。

    对于 $i \geq 2$,项 $[a_i < x \leq a_{i-1}]$ 仅在 $a_i < a_{i-1}$ 时非零,此时它对 $x \in (a_i, a_{i-1}]$ 贡献 $1$。

    计算变化量

    在临界点 $x = a_j + 1$ 处,$f$ 的变化量 $\Delta f(j)$ 为:

    $$\Delta f(j) = [j=1] + [j \geq 2 \land a_j < a_{j-1}] - [j \leq N-1 \land a_j > a_{j+1}]$$

    解释:

  • $[j=1]$:第一个灯亮起时,产生新段

  • $[a_j < a_{j-1}]$:当 $a_j < a_{j-1}$,灯 $j$ 比灯 $j-1$ 先亮,产生新段
  • $-[a_j > a_{j+1}]$:当 $a_j > a_{j+1}$,灯 $j+1$ 比灯 $j$ 先亮,灯 $j$ 亮起时连接两个段,段数减 $1$

    数据结构

    将所有可能的临界点 $a_i + 1$ 和所有修改中的 $v + 1$ 离散化。

    建立线段树,维护每个离散化位置上的 $\Delta f$ 值。线段树节点存储:

  • sum:区间总和

  • max_prefix:区间最大前缀和

    查询操作:找最小的 $x$ 使 $f(x) \geq k$,等价于找最小下标使前缀和 $\geq k$。利用 max_prefix 在线段树上二分,时间 $O(\log N)$。

    修改操作:修改 $a_p = v$ 只影响位置 $p-1, p, p+1$ 处的 $\Delta f$ 值。先减去旧值,更新 $a_p$,再加新值。每次影响 $O(1)$ 个位置,时间 $O(\log N)$。

    正确性证明

    引理 1:$f(x)$ 是分段常数函数,仅在 $x = a_i + 1$ 处可能变化。

    证明:灯 $i$ 的状态在 $x = a_i + 1$ 处从灭变亮,其他时刻不变。因此 $f(x)$ 仅在此时刻可能变化。∎

    引理 2:$\Delta f(j) = [j=1] + [j \geq 2 \land a_j < a[j-1]] - [j \leq N-1 \land a_j > a_{j+1}]$。

    证明:在 $x = a_j + 1$ 处,灯 $j$ 亮起。若 $j=1$ 或 $a_j < a_{j-1}$,灯 $j-1$ 未亮,产生新段,$+1$。若 $j=N$ 或 $a_j \leq a_{j+1}$,灯 $j+1$ 未亮,不影响。若 $a_j > a_{j+1}$,灯 $j+1$ 已亮,灯 $j$ 亮起连接左右两段,段数减 $1$。∎

    引理 3:对于任意 $x$,$f(x) = \sum_{j: a_j + 1 \leq x} \Delta f(j)$。

    证明:由引理 1,$f(x)$ 从 $f(0)=0$ 开始,在每个临界点 $a_j+1$ 处增加 $\Delta f(j)$。累加即得。∎

    引理 4:修改 $a_p$ 只影响 $\Delta f(p-1), \Delta f(p), \Delta f(p+1)$。

    证明:$\Delta f(j)$ 仅依赖于 $a_{j-1}, a_j, a_{j+1}$。修改 $a_p$ 只可能影响 $j \in {p-1, p, p+1}$ 的表达式。∎

    定理:对于每次查询 $2\ k$,算法输出最小的 $x$ 使得 $f(x) \geq k$,或 $-1$。

    证明:由引理 3,$f(x)$ 等于临界点 $\leq x$ 的 $\Delta f$ 之和。线段树维护了这些 $\Delta f$ 的前缀和。find_first(k) 返回最小下标 $i$ 使前缀和 $\geq k$,对应临界点 $c_i$ 即为最小 $x$。若最大前缀和 $< k$,则不存在满足条件的 $x$,输出 $-1$。∎

    复杂度分析

  • 离散化:$O((N+Q)\log(N+Q))$

  • 初始化:$O(N \log N)$
  • 每次修改:$O(\log N)$
  • 每次查询:$O(\log N)$

    总时间:$O((N+Q)\log(N+Q))$

    空间:$O(N+Q)$

这段代码并没有枚举所有 (x,y),而是枚举一组能够“支配”所有其他方案的候选方案:对于任意未枚举的划分,都能找 到一个枚举到的方案,其补人数不更多。

设三个组原有的人数为:

[ (L,M,H),\qquad L+M+H=n ]

人数为 (v) 的组至少还要补:

[ f(v)=\operatorname{bit_ceil}(v)-v ]

其中 bit_ceil(0)=1,正好表示空组需要补 1 人。

1. 外层循环枚举所有可能的轻量级人数

在 main.cpp:36 的循环开始时:

sum = bucket[0] + ... + bucket[i - 1];

也就是:

[ sum=#{a_k<i+1} ]

因此可以理解为令:

x = i + 1; L = sum;

由于同一体重的人不能被边界拆开,真正有意义的轻量级人数只有这些完整体重桶的前缀和,外层循环全部枚举到了。

L=n 等所有人都在一个组的边界情况,由初始答案:

int ans = least_add_to_pow2(n) + 2;

处理,即另外两个空组各补 1 人。

2. most 表示合法分界点

可以把所有人按体重排好序。代码没有真的排序,而是通过 bucket 构造:

most[pos] = 该位置所属体重段的起始下标

例如排序后的体重为:

1 1 3 3 3 5

则:

位置: 0 1 2 3 4 5 most: 0 0 2 2 2 5

所以:

most[k]

就是不拆开同体重人群的情况下,不超过位置 k 的最大合法前缀分界点。

在轻量级人数为 sum=L 时:

most[L + p] - L

就是不超过 p 的最大合法中量级人数。

3. 为什么只枚举 (p=1,2,4,8,\dots)

关键代码是:

for (int j = 0; 1 << j < n - sum; j++)

也就是只枚举:

[ p=2^j ]

这是因为最终每个组都要扩充到 2 的幂。

假设某个最优方案的中量级人数为 (M),令:

[ p=\operatorname{bit_ceil}(M) ]

那么 (p) 一定是 2 的幂,并且:

[ M\le p ]

代码针对这个 (p) 选择:

[ M'=\text{不超过 }p\text{ 的最大合法中量级人数} ]

也就是:

M' = most[L + p] - L;

因为原来的 (M) 本身就是一个合法分界点且 (M\le p),所以:

[ M'\ge M ]

同时又有 (M'\le p),因此:

[ \operatorname{bit_ceil}(M')\le p =\operatorname{bit_ceil}(M) ]

中量级最终需要的容量不会增加。

另一方面:

[ H'=n-L-M'\le n-L-M=H ]

所以重量级人数减少,其最终需要扩充到的 2 的幂也不会增加:

[ \operatorname{bit_ceil}(H') \le \operatorname{bit_ceil}(H) ]

因此,从任意方案的 (M) 出发,都可以移动到代码枚举的 (M'),且总补人数不会变多。

这就是为什么只考察每个 2 的幂附近的最大合法分界点已经足够。

4. 当对应的 2 的幂超过剩余人数

如果:

[ p\ge n-L ]

那么循环不会访问 most[L+p],因为会越界。此时直接把剩下的人全部放入中量级,重量级为空:

ans = min(ans, least_add_to_pow2(sum) + least_add_to_pow2(n - sum) + 1);

对应:

[ (L,n-L,0) ]

最后的 +1 是给空的重量级补一个人。

这同样不会比原方案更差:剩余全部人数也不超过那个目标 2 的幂,而重量级空组只需补 1 人。

总结

代码覆盖答案的逻辑是:

  1. 外层枚举所有合法的轻量级前缀人数 (L)。
  2. 对于固定的 (L),最终中量级容量必然是某个 (2^j)。
  3. 对每个 (2^j),选择不拆开相同体重的最大合法中量级人数。
  4. 任意其他分界都能被这样的候选方案替代,而且所需总容量不会增加。
  5. 剩余人全部进入中量级、以及所有人在同一组等边界情况被单独处理。

    所以它枚举的不是所有划分,而是“所有可能成为最优解的代表划分”,复杂度为:

    [ O(n\log n) ]