注意,本博客的任何内容不应被滥用、随意转载。

坐电梯 2026.1

  1. P10594 BZOJ2445 最大团

直接计数。图个数为 \(G=\sum\limits_{d|n}\frac{n!}{(d!)^c c!}(c=\frac{n}{d})\),即划分为 \(d\) 个集合,然后再钦定顺序(图是无序的)。

据欧拉定理答案就是 \(m^{G\bmod (P-1)+P-1}\)\(P=10^9-401\) 是质数)。

\(P-1\) 分解成 \(2\times13\times5281\times7283\),对这四个 \(q\) 如下分别算 \(G\bmod q\) 最后 CRT 合并:

枚举 \(d\),把 \(q^{\nu(n!)-c\nu(d!)-\nu(c!)}\) 拆出来,用 exLucas 算 \((k!)_q\),这样就能求逆元,再把 \(q\) 乘回去。

👍
#include<bits/stdc++.h>
#define LL long long
#define fi first
#define se second
using namespace std;

const int N=8e3+10,mod=1e9-401;
int T,n,m,fct[N],k,q,a[4],b[4];
vector<int>f;

int qpow(int x,int y,int P){
    int ret=1;
    while(y){
        if(y&1)ret=1ll*ret*x%P;
        y>>=1;x=1ll*x*x%P;
    }
    return ret;
}
int exgcd(int a,int b,int &x,int &y){
    if(!b)return x=1,y=0,a;
    int r=exgcd(b,a%b,x,y),t=x;
    x=y;
    y=t-a/b*y;
    return r;
}
LL num(int n){
    LL r=0;
    while(n)r+=(n/=q);
    return r;
}
int calc(int n){
    if(!n)return 1;
    return 1ll*calc(n/q)*(n/q%2?q-1:1)%q*fct[n%q]%q;
    //此处q=q^c
}
void cjw(){
    a[k]=q;
    for(int i=1;i<q;i++)fct[i]=1ll*i*fct[i-1]%q;
    int res=0,pw,now;
    for(int i:f){
        pw=num(n)-num(i)*(n/i)-num(n/i);
        if(pw)continue;
        now=1ll*calc(n)*qpow(1ll*qpow(calc(i),n/i,q)*calc(n/i)%q,q-2,q)%q;
        res=(res+now)%q;
    }
    b[k]=res;
    k++;
}
int crt(){
    int A=1,B=0;
    for(int i=0;i<k;i++){
        int p,g=exgcd(A,a[i],p,*new int);
        p=((b[i]-B)/g*p%(a[i]/g)+a[i]/g)%(a[i]/g);
        B+=A*p;
        A=A/g*a[i];
    }
    return B+A;
}

signed main(){
    scanf("%d",&T);
    fct[0]=1;
    while(T--){
        //mod-1=2*13*5281*7283
        k=0;f.clear();
        scanf("%d%d",&n,&m);
        for(int i=1;i*i<=n;i++)if(n%i==0){
            f.push_back(i);
            if(i*i!=n)f.push_back(n/i);
        }
        q=2;cjw();
        q=13;cjw();
        q=5281;cjw();
        q=7283;cjw();
        printf("%d\n",qpow(m,crt(),mod));
    }
    return 0;
}
  1. *Q1839 Joke

\(p,q\) 指定了上下两行的取值顺序,\(s\) 则指定了序列间的拓扑序。只要无环就存在 \(a\)

考虑如果 \(p,q,s\) 都已知,三者同时按 \(p\) 重排没有影响(重标号不改变图形态)。具体考察环存在性:

只有 \(i<j,q_i>q_j,s_i=1,s_j=0\) 这一种情况,我们要求它不存在。

多维偏序,把 \((i,q_i)\) 画到平面上

如果选中一个点为 \(s=1\),那么其右下方必须全为 \(1\)。因此,一个上升子序列可以唯一描述一个合法方案。一个合法方案也可以简单构造出一个上升子序列。故求所有 \(q\) 的上升子序列个数和

\(f_{i,j,k}\) 表示前 \(i\) 个数,有 \(j\)\(0\) 被钦定值并选入子序列,子序列最后一个数是 \(k\) 的方案数,前缀和优化可以简单求解。最后把未选入的位置数学统计下。由于要求是上升子序列,选中的 \(0\) 不会被钦定为相等值,只需令非 \(0\) 的确定位值不重复出现。

👍
#include<bits/stdc++.h>
#define LL long long
#define fi first
#define se second
using namespace std;

const int N=5e2+10,mod=998244353;
int n,p[N],q[N],rq[N],ans,f[N][N][N],m,fct[N];

void add(int &x,int y){if((x+=y)>=mod)x-=mod;}

signed main(){
    freopen("permutation.in","r",stdin);
    freopen("permutation.out","w",stdout);
    scanf("%d",&n);
    fct[0]=1;
    for(int i=1;i<=n;i++)scanf("%d",&p[i]),fct[i]=1ll*i*fct[i-1]%mod;
    for(int i=1;i<=n;i++)scanf("%d",&q[p[i]]);
    for(int i=1;i<=n;i++)rq[q[i]]=i,m+=!!q[i];
    for(int i=0;i<=n;i++)f[0][0][i]=1;
    for(int i=1;i<=n;i++)
        for(int j=0;j<=i;j++)
            for(int k=0;k<=n;k++){
                if(k)f[i][j][k]=f[i][j][k-1];
                add(f[i][j][k],(f[i-1][j][k]-(k?f[i-1][j][k-1]:0)+mod)%mod);
                if(q[i]&&k==q[i])add(f[i][j][k],f[i-1][j][k-1]);
                if(!q[i]&&j&&k&&!rq[k])add(f[i][j][k],f[i-1][j-1][k-1]);
            }
    for(int i=0;i<=n-m;i++)
        add(ans,1ll*f[n][i][n]*fct[n-m-i]%mod);
    printf("%d\n",ans);
    return 0;
}

【已编辑】

吃完【菌菇酥肉火烧米线】不再发电。

如果我能走到知识较完备、水平冲省队的那一天,可以多看看各种人写的冷门技巧、自发推广。

比如BSGS 杂技——平衡复杂度的艺术


解除 * 使用。

  1. AT_abc163_e Active Infants

完全无法 dp,找结构。

考虑最大值 \(a\),如果存在 \(b\) 于答案中和 \(a\) 同向移动并且更远,把 \(a,b\) 终点互换更优。

如果 \(a\) 没有移动到答案两端,上述情况一定出现。所以策略是:将值从大到小依次放在最左侧或最右侧。

可以区间 dp 了。倒着来等于是从小到大自中间开始向两端放置,更方便。

👍
#include<bits/stdc++.h>
#define LL long long
#define fi first
#define se second
using namespace std;

const int N=2e3+10;
int n;
LL f[N][N];
pair<int,int>a[N];

signed main(){
    //freopen(".in","r",stdin);
    //freopen(".out","w",stdout);
    scanf("%d",&n);
    for(int i=1;i<=n;i++)scanf("%d",&a[i].fi),a[i].se=i;
    sort(a+1,a+n+1);
    for(int l=1;l<=n;l++){
        for(int i=1;i+l-1<=n;i++){
            int j=i+l-1;
            f[i][j]=max(f[i+1][j]+1ll*a[l].fi*abs(i-a[l].se)
            ,f[i][j-1]+1ll*a[l].fi*abs(j-a[l].se));
        }
    }
    printf("%lld\n",f[1][n]);
    return 0;
}
  1. CF1845E Boxes and Balls

由于做过 P9963 我直接往间隔长度去想,\(n^3\) 坠。

重新考虑代价计算的方式:初始 \(1\) 位置序列是 \(\{a_m\}\),操作成 \(\{b_m\}\) 的话,代价为 \(s=\sum |a_i-b_i|\)。这里是最小次数,简单发现该方案满足 \(k\ge s,2|k-s\) 就能做到恰好 \(k\) 次。

拆贡献。设 \(a\)\(c_1\)\(1\)\([1,i]\) 中,\(b\)\(c_2\) 个,那么 \(i\leftrightarrow i+1\) 这个间隙最终恰好被经过 \(|c_1-c_2|\) 次,同样可用于计算 \(s=\bf{\sum|\Delta|}\)。如果直接从左到右在 dp 中构造出最终状态,可以见得 \(|\Delta|\) 每次至多 \(\bf{+1}\)

此处应有应激:\(|\Delta|\le2\sqrt{k}\)

这样就能做了,记录 \(f_{i,s,\Delta}\) 容易转移。答案按上述统计。

👍
#include<bits/stdc++.h>
#define LL long long
#define fi first
#define se second
using namespace std;

const int N=1510,mod=1e9+7;
int n,m,a[N],f[2][N][120],s[N];

void add(int &x,int y){if((x+=y)>=mod)x-=mod;}

signed main(){
    //freopen(".in","r",stdin);
    //freopen(".out","w",stdout);
    scanf("%d%d",&n,&m);
    for(int i=1;i<=n;i++)scanf("%d",&a[i]),s[i]=s[i-1]+a[i];
    int M=sqrt(m*2);
    f[0][0][M]=1;
    for(int i=1;i<=n;i++)
        for(int j=0;j<=m;j++)
            for(int k=-M;k<=M;k++){
                if(j+abs(k+1-a[i])<=m&&-M<=k+1-a[i]&&k+1-a[i]<=M)
                    add(f[i&1][j+abs(k+1-a[i])][k+1-a[i]+M],f[i&1^1][j][k+M]);
                if(j+abs(k-a[i])<=m&&-M<=k-a[i]&&k-a[i]<=M)
                    add(f[i&1][j+abs(k-a[i])][k-a[i]+M],f[i&1^1][j][k+M]);
                f[i&1^1][j][k+M]=0;
            }
    int ans=0;
    for(int i=m%2;i<=m;i+=2)add(ans,f[n&1][i][M]);
    printf("%d\n",ans);
    return 0;
}

场上过的很少,大概是因为这题可行的暴力状态很多。

  1. CF2175D Wishing Cards

这题兑很多人来说应该非常水,但是我注意了很久才注意到。

要求前缀最大值之和,那么不是前缀最大值的数很可能没有用。仔细考量:如果 \(a\) 中存在一个非严格逆序对 \(i<j,a_i>a_j\),把 \(b_j\) 全部加给 \(b_i\) 一定不劣。因此有值的一定是 \(a\) 序列中的严格前缀最大值位置。

就卡在这里了。想了一晚上才注意到:严格前缀最大值位置数是 \(\bf\min(V,n)\),这题 \(V\le 360\)

那直接 dp 了哈哈,做到 \(V^3\)。给 dp 数组一个前缀最大值优化就行了,绷。

👍
#include<bits/stdc++.h>
#define LL long long
#define fi first
#define se second
using namespace std;

const int N=1e5+10,M=400;
int n,m,a[N],f[M][M][M],k,s[M][M][M];
pair<int,int>b[M];

signed main(){
    //freopen(".in","r",stdin);
    //freopen(".out","w",stdout);
    int T;scanf("%d",&T);
    while(T--){
        scanf("%d%d",&n,&k);
        m=0;
        for(int i=1,mx=0;i<=n;i++){
            scanf("%d",&a[i]);
            if(a[i]>mx)b[++m]={i,a[i]},mx=a[i];
        }
        b[m+1]={n+1,0};
        for(int i=0;i<=k;i++)for(int j=0;j<=b[m].se;j++)f[0][i][j]=-1e9;
        f[0][0][0]=0;
        for(int i=0;i<=b[m].se;i++)s[0][0][i]=0;
        for(int i=1;i<=m;i++){
            for(int j=0;j<=k;j++)
                for(int y=0;y<=b[m].se;y++){
                    if(y<=b[i].se)f[i][j][y]=max(j>=y&&y?s[i-1][j-y][y-1]:(int)-1e9,f[i-1][j][y])+(b[i+1].fi-b[i].fi)*y;
                    else f[i][j][y]=-1e9;
                    s[i][j][y]=max(y?s[i][j][y-1]:(int)-1e9,f[i][j][y]);
                }
        }
        printf("%d\n",s[m][k][b[m].se]);
    }
    return 0;
}
  1. AT_cf17_final_j Tree MST

非常经典,完全图直接用 boruvka。考虑求出关于一个点的最短边。

给出了树形态,考虑树形 dp。比较容易求出子树中与自己距离(题面中的边权)最近的点。换根也很简单,dp 时记录子节点转移值的前后缀 min 即可。

由于要求所在联通块不同,把 dp 值改为记录最近点、所在联通块不同于最近点的次近点。

然后就是正常 brvk 流程,复杂度 \(\Theta(n\log^2 n)\),因为有并查集。

👍
#include<bits/stdc++.h>
#define LL long long
#define fi first
#define se second
using namespace std;

const int N=2e5+10;
int n,a[N],m,t[N],W[N];
vector<pair<int,int>>e[N];
struct node{LL p1,p2;int c1,c2;}f[N],g[N];
vector<node>l[N],r[N];
bool frst=1,z[N];
node w[N];
LL ans;
pair<LL,int>F[4];

void op(node &A,node &B,int w){
    F[0]={A.p1,A.c1};
    F[1]={A.p2,A.c2};
    F[2]={B.p1+w,B.c1};
    F[3]={B.p2+w,B.c2};
    sort(F,F+4);
    A.c1=A.c2=0;
    for(int i=0;i<4;i++)
        if(!A.c1)A.c1=F[i].se,A.p1=F[i].fi;
        else if(A.c1!=F[i].se){A.c2=F[i].se,A.p2=F[i].fi;break;}
}

int gf(int x){
    return x==t[x]?x:(t[x]=gf(t[x]));
}

void dfs1(int u,int fa){
    f[u]={a[u],(LL)1e18,gf(u),0};
    for(auto E:e[u]){
        int v=E.fi;
        if(v!=fa)dfs1(v,u);
        else W[u]=E.se;
    }
    if(frst){
        if(fa)e[u].erase(lower_bound(e[u].begin(),e[u].end(),make_pair(fa,0)));
        l[u].resize(e[u].size());
        r[u].resize(e[u].size());
    }
    if(e[u].empty())return;
    l[u][0]={(LL)1e18,(LL)1e18,0,0};op(l[u][0],f[e[u][0].fi],e[u][0].se);
    r[u][e[u].size()-1]={(LL)1e18,(LL)1e18,0,0};op(r[u][e[u].size()-1],f[e[u].back().fi],e[u].back().se);
    
    for(int i=1;i<e[u].size();i++)l[u][i]=l[u][i-1],op(l[u][i],f[e[u][i].fi],e[u][i].se);
    for(int i=e[u].size()-2;i>=0;i--)r[u][i]=r[u][i+1],op(r[u][i],f[e[u][i].fi],e[u][i].se);
    op(f[u],r[u][0],0);
}
void dfs2(int u){
    // printf("%d\n",u);
    op(f[u],g[u],W[u]);
    for(int i=0;i<e[u].size();i++){
        int v=e[u][i].fi,w=e[u][i].se;
        g[v]={a[u],(LL)1e18,gf(u),0};
        op(g[v],g[u],W[u]);
        if(i)op(g[v],l[u][i-1],0);
        if(i<e[u].size()-1)op(g[v],r[u][i+1],0);
        dfs2(v);
    }
}

signed main(){
    scanf("%d",&n);
    for(int i=1;i<=n;i++)scanf("%d",&a[i]),t[i]=i;
    for(int i=1,x,y,z;i<n;i++)
        scanf("%d%d%d",&x,&y,&z),e[x].push_back({y,z}),e[y].push_back({x,z});
    for(int i=1;i<=n;i++)sort(e[i].begin(),e[i].end());
    m=n;
    while(m>1){
        for(int i=1;i<=n;i++)w[i]=g[i]={(LL)1e18,(LL)1e18,0,0};
        dfs1(1,0);frst=0;
        dfs2(1);
        for(int i=1;i<=n;i++)op(w[gf(i)],f[i],a[i]),z[i]=i==gf(i);

        for(int i=1;i<=n;i++)if(z[i]){
            int y;LL x;
            w[i].c1=gf(w[i].c1);w[i].c2=gf(w[i].c2);
            if(w[i].c1!=gf(i))x=w[i].p1,y=w[i].c1;
            else x=w[i].p2,y=w[i].c2;

            if(y!=gf(i))t[y]=i,ans+=x,m--;
        }
    }
    printf("%lld\n",ans);
    return 0;
}
  • 这种做法十分常规。P2076(脑残卡常)和 CF888G 为例题。
  1. ABC411
  • E:求全局最大值,所以全部放一起排序。然后对 \(x\)\(\max \le x\) 的方案数是简单的(每个骰子在前缀中的值个数乘积,求出 \(1\sim 6\) 逆元简单维护),差分带权求和。
  • G:模板环计数。二元环分开做。多元环状压 dp 求 \(s\)\(t\) 的路径数再加入 \((s,t)\) 这条边。这样 \(2^n n^3\),不行。这题的东西仍然经典:钦定环在最小点计算。这样复杂度变成 \(\sum 2^ii^2< 2^{n+1}n^2\) 就赢了,相当跑不满。
  1. ABC410
  • F:牛魔再做不出这种题我去世了,脑残吧。设 \(m<n\),直接得到 \(m\le \sqrt{nm}\), 那么 \(nm^2\) 就是合理的,这样能获得一行两个点,把二维前缀和的式子移项,前面维护、后面枚举。直接统计。脑残。脑残。

    就这样一个小技巧,像是一类肿瘤一样,长在各路出题人脑子里。梦熊出一次,CF 出一次,AT 还有一次。

  • G:这题真好。先在此假设 \(a_i<b_i\)。环很麻烦,考虑在 \(1\) 处破开,那么答案形如

    \(b\) 排序,求 \(f_i\) 表示前 \(i\) 个弦的最长嵌套(\(a_i<a_j<b_j<b_i\))链,可以用树状数组维护。枚举最后一个红色弦 \(r\),求出最后一个可能的蓝色弦 \(l\) 使 \(b_l<a_r\),答案是 \(f_l+f_r\),取最大值。

为什么中间跳过了几场?因为没价值或偏难。

ABC409G:很复杂的概率式子推导,但主要还是由于需要用到 NTT 而我还不会。

ABC408G:用到 SBT。LCA 讲了但我仍然不会,留着以后来。

  1. ABC407
  • F:非常平庸。用单调栈可以直接做,只需要枚举短的那边然后用差分实现区间加,笛卡尔树能够分析出这是一个 \(\log\)。不过差分后的单点加居然也是区间加,可以二阶差分做到线性。
  • G:用到网络流(费用流),我需要重新学习。
  • E:这题挺不错。似乎早年出现在模拟赛中过,这一次自己做出来也并非什么成就。不可能 dp,这很明显。从左到右贪心,先假设选右括号,只要非法就将左侧权值最大的右括号改为左括号。最后一定获得合法括号串,最优证明是该策略和官方题解实则一致。
  1. ABC405
  • G:莫队。扔。

  • E:已经存在有三道我没做出来的、需要精细考虑的题目:AT_abc279_g At Most 2 ColorsP14636 NOIP2025 sale、这题。比较有特征的是,\(1\)\(3\) 的位置关系与 \(2\)\(4\) 的位置关系是相似且偏强力的。

    可以发现,只需要确定 \(1,3\) 位置,\(2,4\) 就只能从左往右依次放置。在保证 \(4\)\(1\) 右侧的前提下,枚举 \(1\) 的末尾 \(i\le a+b\),再将 \(1,3\) 放置。预处理组合数容易模拟。

  • F:同样很经典。两个弦相交当且仅当这两个区间相交且不包含。离线二维数点。

1.8

可以见得,现存有很多难题因为知识点不足而难以投入思考,但是就我在 NOIP2025 BC 中的表现来看,我实际上于可思考题也并没有达到完备的水准。

对于知识点,我只有一步一步学。但题目?我希望找一些不要充斥着未知难度板子的难题。这样对我达到有限完备水准更有帮助。

关于这一点可以和 LCA 进一步交流。


  1. AT_abc225_f String Cards

仍然非常经典。

考虑一个答案 \(\{a_m\}\),如果存在 \(s_{a_{i+1}}+s_{a_{i}}<s_{a_i}+s_{a_{i+1}}\),可以交换这两项得到更优解(注意这里如果考虑 \((i,j)\) 而非 \((i,i+1)\) 无法得到结论)。

那么我们就尝试重载 cmp 为 \([x+y<y+x]\),如果这是一个严格弱序关系,我们就可以做到 dp。

可以证明 cmp 等价于 \([x^\infty<y^\infty]\),确实是严格弱序。具体证明不难,不过另有结论:若 \(x+y=y+x\),则 \(x,y\) 均有长度为 \(\gcd(|x|,|y|)\) 的相等循环节。

排序后 dp。考虑到字典序比较从头开始,可以倒序 dp,这样构造解的时候容易比较。

👍
#include<bits/stdc++.h>
#define LL long long
#define fi first
#define se second
using namespace std;

const int N=60;
int n,m;
string f[N][N],a[N];

signed main(){
    //freopen(".in","r",stdin);
    //freopen(".out","w",stdout);
    scanf("%d%d",&n,&m);
    for(int i=1;i<=n;i++)cin>>a[i];
    sort(a+1,a+n+1,[](string x,string y){return x+y<y+x;});
    for(int i=n;i>=1;i--){
        for(int j=1;j<=min(n-i+1,m);j++)
            f[i][j]=min(a[i]+f[i+1][j-1],(j<n-i+1?f[i+1][j]:"~"));
    }
    cout<<f[1][m]<<'\n';
    return 0;
}
  1. AT_agc003_c BBuBBBlesort!

一个很直接的想法是奇偶位置分别重排,然后再求逆序对数。

然后就错了。原因是 1 操作中途可能可以用 2 操作节省两次。

从单点贡献上重新考虑,一个数现在位于 \(i\),最终位于 \(j\),那么若 \(2|(i-j)\),不需要 1 操作。否则需要恰好一次来改变奇偶性。

👍
#include<bits/stdc++.h>
#define LL long long
#define fi first
#define se second
using namespace std;

const int N=1e5+10;
int n,a[N],w[N];

signed main(){
    // freopen("01.txt","r",stdin);
    // freopen("a.out","w",stdout);
    scanf("%d",&n);
    for(int i=1;i<=n;i++){
        scanf("%d",&a[i]);
        w[i]=a[i];
    }
    sort(w+1,w+n+1);
    for(int i=1;i<=n;i++)a[i]=lower_bound(w+1,w+n+1,a[i])-w;
    int ans=0;
    for(int i=1;i<=n;i++)ans+=abs(i-a[i])%2;
    printf("%d\n",ans/2);
    return 0;
}
  1. AT_agc003_e Sequential operations on Sequence

先剔除一些无用信息:如果存在 \(a_i\ge a_j\),可以见得 \(a_i\) 无用。所以在提前将 \(n\) 设为 \(a_0\) 后仅保留严格后缀最小值,放入 \(\{b_m\}\) 数组,翻转后 \(b\) 递减。

通过模拟发现,题面中前缀截取多次后可能会使序列非常复杂。不能用常规手段维护。

于是尝试单步归纳。考虑最终态 \(b_1\),它由 \(b_{2}\) 重复 \(\lfloor\frac{b_{1}}{b_{2}}\rfloor\) 次、后接 \(b_2\)\([1,b_1\bmod b_2]\) 这一前缀组成。

\(x=b_1\bmod b_2\),则 \(b_2\) 中的 \([1,x]\)\(b_3\) 重复 \(\lfloor\frac{x}{b_3}\rfloor\) 次,后接 \(b_3\)\([1,x\bmod b_3]\) 这一前缀组成。

……

类似地推广,如果 \(b_i\)\(b_{[1,i-1]}\) 重复了 \(c_i\) 次,按上述方式计算 \(i\)重复\(b_j(j>i)\) 重复次数的贡献。

依次对 \(b_j(j>i)\) 取模完毕的 \(x\),表示后接 \(\{1,2,3,\dots,b_m\}\) 这一序列的 \([1,x]\) 前缀,运用差分将 \(s_{[1,x]}\) 加上 \(c_i\) 即可。

关于此流程的模拟,可以用二分找到第一个 \(b_k\le x\)\(k\),令 \(x\overset\bmod\gets b_k\) 并计算贡献。可以分析复杂度为 \(\Theta(n\log V\log n)\)

\(s\) 数组即答案。

👍
#include<bits/stdc++.h>
#define LL long long
#define fi first
#define se second
using namespace std;

const int N=1e5+10;
int n,m;
LL len,a[N],mn[N],b[N],c[N],s[N];

signed main(){
    scanf("%lld%d",&a[1],&n);
    for(int i=2;i<=n+1;i++)scanf("%lld",&a[i]);
    n++;
    mn[n+1]=2e18;
    for(int i=n;i>=1;i--){
        mn[i]=min(mn[i+1],a[i]);
        if(a[i]<mn[i+1])b[++m]=a[i];
    }
    int L,R,mid,j;
    c[1]=1;
    for(int i=1;i<=m;i++){
        LL x=b[i],j=i;
        while(j<m){
            L=j+1;R=m;j=m;
            while(L<=R){
                mid=L+R>>1;
                if(b[mid]<=x)j=mid,R=mid-1;
                else L=mid+1;
            }
            c[j]+=c[i]*(x/b[j]);
            x%=b[j];
        }
        s[1]+=c[i];s[x+1]-=c[i];
    }
    for(int i=1;i<=a[1];i++)s[i]+=s[i-1],printf("%lld\n",s[i]);
    return 0;
}

这道题存在有非常好的子问题分治结构。

  1. AT_agc004_b Colorful Slimes

这是一个环,那么先想办法断开。可以见得最小值一定是自己直接买,以其为起点。

枚举轮换次数 \(i\),那么对于 \(j\),不可能从 \([1,j-i)\) 的位置轮换过来。而 \([j-i,j]\) 中的任意位置 \(k\),都可以选择在第 \(j-k\) 次轮换前买入,然后恰好轮换到 \(j\)

所以 \(j\) 处付出的代价是 \(\min\limits_{k=j-i}^j a_k\),直接模拟。用笛卡尔树来差分算贡献应该能做到线性。

👍
#include<bits/stdc++.h>
#define LL long long
#define fi first
#define se second
using namespace std;

const int N=4e3+10;
int n,a[N],m,q[N],l,r;
LL ans=1e18;

signed main(){
    //freopen(".in","r",stdin);
    //freopen(".out","w",stdout);
    scanf("%d%d",&n,&m);
    int st=1;
    for(int i=1;i<=n;i++)scanf("%d",&a[i]),st=a[i]<a[st]?i:st,a[i+n]=a[i];
    if(n==1)return printf("%d\n",a[1]),0;
    for(int i=0;i<n;i++){
        LL res=0;l=1;r=0;
        for(int j=st;j<st+n;j++){
            while(l<=r&&a[q[r]]>=a[j])r--;q[++r]=j;
            while(l<=r&&q[l]<j-i)l++;
            res+=a[q[l]];
        }
        ans=min(ans,1ll*i*m+res);
    }
    printf("%lld\n",ans);
    return 0;
}

此前选择用 dp 来算答案,导致优化不了,又盲猜一手三分,还写一遍退火,直接坠。

  1. AT_agc004_e Salvage Robots

令人惊讶的是,这居然是铜牌题。

首先转化成移动出口,在出口矩形外的就死了。然后由于限制都表示为矩形,合理试证最后的路径也是走完一个矩形。易证。

状态就有了。\(f_{u,d,l,r}\) 就是表示走完了一个矩形的答案(注意不一定救了其中的所有机器人)。接下来是拓展一行或一列。比如拓展上端,那么可以救下第 \(u-1\) 行的 \([l,r]\) 中未被摧毁的机器人。目前未被摧毁的位置集是一个容易计算的矩形,求交算答案。

👍
#include<bits/stdc++.h>
#define LL long long
#define fi first
#define se second
using namespace std;

const int N=1e2+10;
int n,m,U,D,L,R,sx,sy,r;
short f[N][N][N][N],cr[N][N],cc[N][N],gu,gd,gl,gr,ans;
char a[N][N];

void Max(short &x,short y){
    x=max(x,y);
}

signed main(){
    //freopen(".in","r",stdin);
    //freopen(".out","w",stdout);
    scanf("%d%d",&n,&m);
    for(int i=1;i<=n;i++){
        scanf("%s",a[i]+1);
        for(int j=1;j<=m;j++)
            if(a[i][j]=='E')f[i][i][j][j]=0,sx=i,sy=j;
            else{
                f[i][i][j][j]=-3e4;
                if(a[i][j]=='o')cr[i][j]=cc[i][j]=1;
            }
    }
    for(int i=1;i<=n;i++)
        for(int j=1;j<=m;j++)
            cr[i][j]+=cr[i][j-1];
    for(int i=1;i<=m;i++)
        for(int j=1;j<=n;j++)
            cc[j][i]+=cc[j-1][i];
    for(int s=0;s<=n-1+m-1;s++){
        for(int u=1;u<=sx;u++)
            for(int d=sx;d<=n;d++)
                for(int l=1;l<=sy;l++){
                    r=s-d+u+l;
                    if(r<sy||r>m)continue;
                    U=1+d-sx;D=n-(sx-u);L=1+r-sy;R=m-(sy-l);
                    // printf("%d %d %d %d %d %d %d %d %hd\n",U,D,L,R,u,d,l,r,f[u][d][l][r]);
                    if(u>1){
                        if(u>U)gu=cr[u-1][min(r,R)]-cr[u-1][max(l,L)-1];
                        else gu=0;
                        Max(f[u-1][d][l][r],f[u][d][l][r]+max((short)0,gu));
                    }
                    if(d<n){
                        if(d<D)gd=cr[d+1][min(r,R)]-cr[d+1][max(l,L)-1];
                        else gd=0;
                        Max(f[u][d+1][l][r],f[u][d][l][r]+max((short)0,gd));
                    }
                    if(l>1){
                        if(l>L)gl=cc[min(d,D)][l-1]-cc[max(u,U)-1][l-1];
                        else gl=0;
                        Max(f[u][d][l-1][r],f[u][d][l][r]+max((short)0,gl));
                    }
                    if(r<m){
                        if(r<R)gr=cc[min(d,D)][r+1]-cc[max(u,U)-1][r+1];
                        else gr=0;
                        Max(f[u][d][l][r+1],f[u][d][l][r]+max((short)0,gr));
                    }
                    Max(ans,f[u][d][l][r]);
                }
    }
    printf("%hd\n",ans);
    return 0;
}
  1. CF2110E Melody

首先,这是一个构造问题。其次,每个音只用一次。那肯定不是让你挑战 NPC,所以大概和欧拉路径有关。

我们试着建模。比较容易察觉的结构是,音量和音高应是严格交替改变的。考虑相邻两个音,它们有某一维是相等的;相似的是关于一个点,出入的两条边有一端是相同的

那么将音量和音高放在左右部,离散化后将一个音的两维连边,跑欧拉路径就可以。这样就能够满足以上条件。

👍
#include<bits/stdc++.h>
#define LL long long
#define fi first
#define se second
using namespace std;

const int N=4e5+10;
int T,n,a[N],b[N],d[N],w[N],s[N],t,h[N];
vector<pair<int,int>>e[N];
bool vis[N];

void dfs(int u){
    for(int i=h[u];i<e[u].size();i=h[u]){
        h[u]++;
        int v=e[u][i].fi,w=e[u][i].se;
        if(!vis[w])vis[w]=1,dfs(v),s[++t]=w;
    }
}

void solve(){
    scanf("%d",&n);t=0;
    for(int i=1;i<=n;i++)
        scanf("%d%d",&a[i],&b[i]),b[i]+=1e9,w[i*2-1]=a[i],w[i*2]=b[i];
    for(int i=1;i<=n+n;i++)
        e[i].clear(),d[i]=0;
    sort(w+1,w+n+n+1);
    for(int i=1;i<=n;i++){
        h[i]=h[i+n]=0;
        a[i]=lower_bound(w+1,w+n+n+1,a[i])-w;
        b[i]=lower_bound(w+1,w+n+n+1,b[i])-w;
        e[a[i]].push_back({b[i],i});e[b[i]].push_back({a[i],i});
        d[a[i]]++;d[b[i]]++;
        vis[i]=0;
        // printf("%d %d\n",a[i],b[i]);
    }
    int cnt=0,p=0;
    for(int i=1;i<=n+n;i++)if(d[i]&1)cnt++,p=i;
    if(cnt!=0&&cnt!=2)return puts("NO"),void();
    if(cnt==0){for(int i=1;i<=n;i++)if(d[i])p=i;}
    dfs(p);
    if(t<n)return puts("NO"),void();
    puts("YES");
    for(int i=t;i>=1;i--)printf("%d ",s[i]);puts("");
}

signed main(){
    //freopen(".in","r",stdin);
    //freopen(".out","w",stdout);
    scanf("%d",&T);
    while(T--)solve();
    return 0;
}
  1. P14780 COCI 2025/2026 #3 国家 / Drzava

简单的暴力是 \(n^3\),即对于每一个根都按题意 dp。

然后就可以换根,换根的复杂度稍加分析可以知道,上界是所有路径长度和。复杂度 \(n^2L\)

不过如果认真地推理,可以发现如果确定了所有城市位置:对于首都,合法位置集合的结构是相当简单的。你可以用以下流程推理:

  • \(1\) 为根。
  • 如果有一个城市在另一对城市的路径上,首都无解。
  • 如果不存在祖先后代关系,首都只要不放在城市子树里就好了。
  • 否则存在,那么一定有一个 \(u\) 作为祖先城市,其余所有城市均在其某一个子树里,首都应放在 \(u\) 子树中、非其余城市子树中的任何一个位置
  • 如果求出 \(f_{i,j},g_{i,j}\) 分别表示在 \(i\) 子树中放置 \(j\) 个城市且不存在祖先后代关系的方案数、所有方案的城市子树大小和,以上答案都能够统计。
  • 当只有一个城市,以上结论有可能不成立,手动计算。
👍
#include<bits/stdc++.h>
#define LL long long
#define fi first
#define se second
using namespace std;

const int N=3e3+10,mod=1e9+7;
int n,f[N][N],g[N][N],sz[N],ans,t[N];
vector<int>e[N];

void add(int &x,int y){if((x+=y)>=mod)x-=mod;}

void dfs(int u,int fa){
    sz[u]=1;
    f[u][0]=1;
    for(int v:e[u])if(v!=fa){
        dfs(v,u);
        for(int i=sz[u];i>=0;i--)
            for(int j=sz[v];j>0;j--)
                add(f[u][i+j],1ll*f[u][i]*f[v][j]%mod),
                add(g[u][i+j],(1ll*f[u][i]*g[v][j]+1ll*f[v][j]*g[u][i])%mod);
        sz[u]+=sz[v];
        for(int i=1;i<=sz[v];i++)add(t[i+2],(1ll*f[v][i]*sz[v]%mod-g[v][i]+mod)%mod);
    }
    if(u==1)for(int i=2;i<n;i++)add(t[i+1],(1ll*f[1][i]*n%mod-g[1][i]+mod)%mod);
    add(f[u][1],1);add(g[u][1],sz[u]);
}

signed main(){
    freopen("count.in","r",stdin);
    freopen("count.out","w",stdout);
    scanf("%d",&n);
    for(int i=1,x,y;i<n;i++)scanf("%d%d",&x,&y),e[x].push_back(y),e[y].push_back(x);
    dfs(1,0);
    t[1]=n;t[2]=1ll*n*(n-1)%mod;
    for(int i=1;i<=n;i++)printf("%d ",t[i]);
    return 0;
}
  1. P10992 蓝桥杯 2023 国 Python A 最长同类子串

这道题是不是有一点点像 P10785 NOI2024 集合

你必须考虑到的一点是,答案有单调性,所以先二分。

那么你现在想要判定,是否在两个串中存在有一对长度为 \(len\) 的子串,使得每一种字母的出现位置集合相同

信息量十分大,可以使用哈希。首先你理应无视字母本身的内容,只关心位置。这里也存在有很多种哈希办法:

  • 把一个位置上的值设为“到下一个相等字母位置的距离”,然后前缀哈希。当子串区间向右滑动一位时,有一段哈希值需要更新。
  • 按字母分离,每一种字母独立做一次前缀哈希(比如:把 x 存在的位置赋为 \(1\),其他位置赋为 \(2\)),再以某种方式复合起来。

只需在 \(s\) 中模拟一次如上方法之一,放入 map;再在 \(t\) 中同样模拟,于 map 中查询 \((i-len,i]\) 的哈希值即可判定。

👍
#include<bits/stdc++.h>
#define LL long long
#define fi first
#define se second
using namespace std;

const int N=1e5+10;
int n,m,p[N],q[26];
char s[N],t[N];
unsigned LL h[N],b[N];
map<unsigned LL,bool>g;

bool chk(int len){
    memset(q,0,sizeof(q));
    g.clear();
    for(int i=1;i<=n;i++){
        p[i]=0;
        if(q[s[i]-'a']){
            p[q[s[i]-'a']]=i-q[s[i]-'a'];
            for(int j=q[s[i]-'a'];j<i;j++)h[j]=h[j-1]*13331+p[j];
        }
        q[s[i]-'a']=i;
        h[i]=h[i-1]*13331;
        if(i>=len)g[h[i]-h[i-len]*b[len]]=1;
    }
    memset(q,0,sizeof(q));
    for(int i=1;i<=m;i++){
        p[i]=0;
        if(q[t[i]-'a']){
            p[q[t[i]-'a']]=i-q[t[i]-'a'];
            for(int j=q[t[i]-'a'];j<i;j++)h[j]=h[j-1]*13331+p[j];
        }
        q[t[i]-'a']=i;
        h[i]=h[i-1]*13331;
        if(i>=len&&g.count(h[i]-h[i-len]*b[len]))return 1;
    }
    return 0;
}

signed main(){
    freopen("string.in","r",stdin);
    freopen("string.out","w",stdout);
    scanf("%s%s",s+1,t+1);n=strlen(s+1);m=strlen(t+1);
    b[0]=1;
    for(int i=1;i<=max(n,m);i++)b[i]=13331*b[i-1];
    int L=0,R=min(n,m),ans=0,mid;
    while(L<=R){
        mid=L+R>>1;
        if(chk(mid))ans=mid,L=mid+1;
        else R=mid-1;
    }
    printf("%d\n",ans);
    return 0;
}
  1. AT_arc212_c ABS Ball

先试着处理 \(a_i,b_i\) 的相消部分。枚举未消部分的总和 \(k=\sum|a-b|\),令剩余 \(n-k\) 个球一红一蓝绑定,即将 \(\frac{(n-k)}2\) 个“球对”放入 \(m\) 个箱子里,允许为空。

然后,每个箱子蓝色多还是红色多并不重要。那么将 \(k\) 个额外的球放入箱子后,有 \(2^m\) 种方式钦定它们的颜色。

最后,想办法把 \(k\) 个球放进去。重新描述:有 \(m\) 个箱子,你要在箱子 \(i\) 里放入 \(x_i\ge 1\) 个球,求每一种满足 \(\sum x_i=k\) 方案的 \(\prod x_i\) 之和。

再次转换。先插 \(m-1\) 个板在 \(k\) 个球的空隙中,再在每对板间分别选出一个代表球。这相当于一共有 \(k+m-1\) 个位置,你要选出 \(2m-1\) 个位置钦定它们交替地作为代表球、插板、代表球……

如上,对于 \(k\ge m,2|(n-k)\),方案数为 \(2^m\binom{\frac{n-k}2+m-1}{m-1}\binom{k+m-1}{2m-1}\),处理组合数。

👍
#include<bits/stdc++.h>
#define LL long long
#define fi first
#define se second
using namespace std;

const int N=2e7+10,mod=998244353;
int n,m,fct[N],inv[N],pw[N],ans;

int qpow(int x,int y){
    int z=1;
    while(y){
        if(y&1)z=1ll*z*x%mod;
        y>>=1;x=1ll*x*x%mod;
    }
    return z;
}
int C(int x,int y){
    if(x<y)return 0;
    return 1ll*fct[x]*inv[x-y]%mod*inv[y]%mod;
}

signed main(){
    //freopen(".in","r",stdin);
    //freopen(".out","w",stdout);
    scanf("%d%d",&n,&m);
    fct[0]=inv[0]=pw[0]=1;
    for(int i=1;i<=n+n;i++)fct[i]=1ll*i*fct[i-1]%mod,pw[i]=2*pw[i-1]%mod;
    inv[n+n]=qpow(fct[n+n],mod-2);
    for(int i=n+n-1;i>=1;i--)inv[i]=1ll*inv[i+1]*(i+1)%mod;
    for(int i=m;i<=n;i++)if((n-i)%2==0)
        ans=(ans+1ll*C((n-i)/2+m-1,m-1)*pw[m]%mod*C(i+m-1,m+m-1))%mod;
    printf("%d\n",ans);
    return 0;
}

这题的关键一招是:把和贡献无关的部分拆出,用数学方法计算。

  1. Q7588 Monster Hunter

务必参考文献,附加模拟赛题

可以发现的是,这篇文章里写的前三个题目,都存在有非常强的顺序限制,并且能够简单地合并信息,无论是题目要求还是调整得到。

我们先考虑这题的顺序限制。

  • 题目给出了树形结构限制。
  • 优先选择打 \(a<b\) 的怪物,其中优先选择 \(a\) 小的点。(调整证明)
  • 其次选择打 \(a\ge b\) 的怪物,其中优先选择 \(b\) 大的点。(调整证明)

据此能够比较两个怪物的信息。

再考虑合并方法。

  • 希望将依次进行 \(x\gets x-a_1+b_1\)\(x\gets x-a_2+b_2\) 合并为一次操作 \(x\gets x-a_3+b_3\)
  • 考察 \(a_3\)。可知打这两个怪物有限制 \(x\ge \max(a_1,a_2+a_1-b_1)\),此限制现由 \(a_3\) 给出。故 \(a_3=a_1+\max(0,a_2-b_1)\)
  • 分讨 \(a_2-b_1\) 的正负性,加之 \(-a_1-a_2+b_1+b_2=-a_3+b_3\),得到 \(b_3=b_2+\max(0,b_1-a_2)\)

据此能进行贪心合并

准许入手。按以上方式对怪物排序,顺序判定。判定怪物 \(i\)

  • 如果 \(fa_i\) 未被打掉,那么在打掉 \(fa_i\) 后必然立刻打 \(i\)(调整证明),合并两者。
  • 否则直接打 \(i\),等同于将其与点 \(1\) 合并。

合并后,可以利用 set 重新维护顺序。

答案即最终的 \(a_1\)

👍
#include<bits/stdc++.h>
#define LL long long
#define fi first
#define se second
using namespace std;

const int N=1e5+10;
int n,T,p[N],f[N];
LL a[N],b[N];
struct node{
    LL x,y;int z;
    const bool operator<(const node &val)const{
        if((val.x<val.y)!=(x<y))return (val.x<val.y)<(x<y);
        else if(x<y)return x==val.x?(y==val.y?z<val.z:y>val.y):x<val.x;
        else return y==val.y?(x==val.x?z<val.z:x<val.x):y>val.y;
    }
};
set<node>s;
vector<int>e[N];

int gf(int x){return x==p[x]?x:(p[x]=gf(p[x]));}

void dfs(int u,int fa){
    f[u]=fa;
    for(int v:e[u])if(v!=fa)dfs(v,u);
}
void solve(){
    scanf("%d",&n);
    e[1].clear();p[1]=1;a[1]=b[1]=0;
    for(int i=2;i<=n;i++)scanf("%lld%lld",&a[i],&b[i]),s.insert({a[i],b[i],i}),p[i]=i,e[i].clear();
    for(int i=1,x,y;i<n;i++)scanf("%d%d",&x,&y),e[x].push_back(y),e[y].push_back(x);
    dfs(1,0);
    while(s.size()){
        int i=s.begin()->z,j=gf(f[i]);s.erase(s.begin());
        if(j>1)s.erase({a[j],b[j],j});
        a[j]+=max(0ll,a[i]-b[j]);
        b[j]=b[i]+max(0ll,b[j]-a[i]);
        p[i]=j;
        if(j>1)s.insert({a[j],b[j],j});
    }
    printf("%lld\n",a[1]);
}

signed main(){
    scanf("%d",&T);
    while(T--)solve();
    return 0;
}
  1. CF1163F Indecisive Taxi Fee

如果改动的边不在原最短路上,求经过 \(e_j\) 的最短路和原最短路的最小值。

否则,答案是经过 \(e_j\) 的最短路与不经过 \(e_j\) 的最短路的最小值。

不经过 \(e_j\) 的最短路,即删边最短路。考虑新最短路形态,和原最短路应有一公共前缀、一公共后缀,枚举新极短路上、不在原最短路的一条边 \((u,v)\)

\(l_u\) 表示所有 \(1\to u\) 最短路与原最短路的最短公共前缀位置。类似地 \(r_v\) 表示所有 \(v\to n\) 最短路与原最短路的最短公共后缀位置。原最短路上的第 \(i\) (0-index)个点 \(g_i\) 初始有 \(l_{g_i}=i,r_{g_i}=i+1\)。在 \(st=1,n\) 的两个最短路 DAG 上分别转移,分别是 \(r_y=\max(r_x,r_y)\)\(l_y=\min(l_y,l_x)\)。注意起点状态已初始化,不允许在原最短路上转移。

\(a_i\) 为删去边 \(e_i\) 后全图的最短路,应操作:\(a_{[l_u+1,r_v-1]}\)\(d_{1,u}+w+d_{v,n}\) checkmin(离线可以用 multiset 扫描维护),即删除这段区间中的边均不影响此极短路。关于正确性:唯一的阻碍在于边两端取局部最短路是否会忽视一些路径。答案是不会。画图证明容易。

然后回到开头,简单输出答案。

👍
#include<bits/stdc++.h>
#define LL long long
#define fi first
#define se second
using namespace std;

const int N=2e5+10;
int n,m,q,st[2],l[N],r[N],h[N],p[N],c[N];
struct edge{int x,y,z;}f[N];
vector<edge>e[N];
vector<pair<LL,bool>>t[N];
vector<int>g;
LL d[2][N],a[N];
multiset<LL>s;

void dijk(int o){
    priority_queue<pair<LL,int>>q;
    for(int i=1;i<=n;i++)d[o][i]=1e18,h[i]=0;
    q.push({d[o][st[o]]=0,st[o]});
    while(q.size()){
        int u=q.top().se;LL D=-q.top().fi;
        q.pop();
        if(D!=d[o][u])continue;
        for(auto E:e[u]){
            int v=E.x,w=E.y;
            if(d[o][v]>D+w)q.push({-(d[o][v]=D+w),v}),h[v]=E.z;
        }
    }
}

signed main(){
    //freopen(".in","r",stdin);
    //freopen(".out","w",stdout);
    scanf("%d%d%d",&n,&m,&q);
    for(int i=1,x,y,z;i<=m;i++){
        scanf("%d%d%d",&x,&y,&z);
        e[x].push_back({y,z,i}),e[y].push_back({x,z,i});
        f[i]={x,y,z};
        c[i]=-1;
    }
    st[0]=1;st[1]=n;dijk(0);dijk(1);
    int k=1;
    while(k){
        g.push_back(k);
        c[h[k]]=g.size();
        k=f[h[k]].x==k?f[h[k]].y:f[h[k]].x;
    }

    for(int i=1;i<=n;i++)l[i]=n;
    for(int i=0;i<g.size();i++)l[g[i]]=i;
    for(int i=1;i<=n;i++)p[i]=i;
    sort(p+1,p+n+1,[](int x,int y){return d[0][x]<d[0][y];});
    for(int u=1;u<=n;u++)for(auto E:e[p[u]]){
        int v=E.x,w=E.y;
        if(c[E.z]==-1&&d[0][v]==d[0][p[u]]+w)l[v]=min(l[v],l[p[u]]);
    }

    for(int i=1;i<=n;i++)r[i]=0;
    for(int i=0;i<g.size();i++)r[g[i]]=i+1;
    sort(p+1,p+n+1,[](int x,int y){return d[1][x]<d[1][y];});
    for(int u=1;u<=n;u++)for(auto E:e[p[u]]){
        int v=E.x,w=E.y;
        if(c[E.z]==-1&&d[1][v]==d[1][p[u]]+w)r[v]=max(r[v],r[p[u]]);
    }

    // for(int i=1;i<=n;i++)printf("%d %d\n",l[i],r[i]);

    for(int i=1;i<=m;i++){
        int x=f[i].x,y=f[i].y,z=f[i].z;
        if(c[i]!=-1||d[0][x]==1e18||d[1][y]==1e18)continue;
        LL D=d[0][x]+z+d[1][y];
        if(l[x]<r[y]-1)t[l[x]+1].push_back({D,1}),t[r[y]].push_back({D,0});
        D=d[1][x]+z+d[0][y];
        if(l[y]<r[x]-1)t[l[y]+1].push_back({D,1}),t[r[x]].push_back({D,0});
    }

    for(int i=1;i<g.size();i++){
        for(auto j:t[i])
            if(j.se)s.insert(j.fi);
            else s.erase(j.fi);
        if(s.size())a[i]=*s.begin();
        else a[i]=1e18;
    }
    while(q--){
        int j,w;scanf("%d%d",&j,&w);
        if(c[j]==-1)
            printf("%lld\n",min(d[0][n],
            min(d[0][f[j].x]+w+d[1][f[j].y],d[0][f[j].y]+w+d[1][f[j].x])));
        else
            printf("%lld\n",min(a[c[j]],
            min(d[0][f[j].x]+w+d[1][f[j].y],d[0][f[j].y]+w+d[1][f[j].x])));
    }
    return 0;
}
  1. CF429E Points and Segments

这个问题没有要求判断无解,所以一定有解。我们可以随意强化成容易构造的样子,只要能反推原问题解就没有关系。

首先一个区间 \([l,r]\) 的所有性质都在 \(l-1,l,r,r+1\) 上,离散化。

考虑一个点如果被覆盖偶数次,则红蓝差为 \(0\);否则为 \(\pm1\)。尝试统一为最具特征的 \(0\),可以在所有被覆盖奇数次的极长区间上加一个区间来实现。

红色区间加,蓝色区间减,要求全局为 \(0\)。依旧差分,如果连无向边 \((l_i,r_i+1)\),正向走表示红,反向走表示蓝,那么解应表示为若干条欧拉回路。容易证明所有点度数在此图中为偶数,欧拉回路存在,按上述染色。

去除额外区间立刻得到原问题解。

👍
#include<bits/stdc++.h>
#define LL long long
#define fi first
#define se second
using namespace std;

const int N=1e6+10;
int n,l[N],r[N],a[N],w[N],m,h[N],s[N],t,nn,c[N];
bool vis[N];
vector<pair<int,int>>e[N];

void dfs(int u){
    for(int i=0;i<e[u].size();i=h[u]){
        h[u]=i+1;
        int v=e[u][i].fi,w=e[u][i].se;
        if(!vis[w])vis[w]=vis[w^1]=1,dfs(v),s[++t]=w;
    }
}

signed main(){
    //freopen(".in","r",stdin);
    //freopen(".out","w",stdout);
    scanf("%d",&n);nn=n;
    for(int i=1;i<=n;i++)
        scanf("%d%d",&l[i],&r[i]),w[++m]=l[i],w[++m]=r[i],w[++m]=l[i]-1,w[++m]=r[i]+1;
    sort(w+1,w+m+1);m=unique(w+1,w+m+1)-w-1;
    for(int i=1;i<=n;i++)
        l[i]=lower_bound(w+1,w+m+1,l[i])-w,r[i]=lower_bound(w+1,w+m+1,r[i])-w,a[l[i]]++,a[r[i]+1]--;
    int L=0,R=0;
    for(int i=1;i<=m+1;i++){
        a[i]+=a[i-1];
        // printf("%d ",a[i]);
        if(a[i]&1){if(L)R=i;else L=R=i;}
        else if(R){n++;l[n]=L;r[n]=R;L=R=0;}
    }
    for(int i=1;i<=n;i++)e[l[i]].push_back({r[i]+1,i*2}),e[r[i]+1].push_back({l[i],i*2+1});
    for(int i=1;i<=m+1;i++)dfs(i),a[i]=0;
    for(int i=1;i<=t;i++)c[s[i]/2]=s[i]%2;
    for(int i=1;i<=nn;i++){
        if(c[i])a[l[i]]++,a[r[i]+1]--;
        else a[l[i]]--,a[r[i]+1]++;
        // printf("%d %d\n",l[i],r[i]);
    }
    for(int i=1;i<=m+1;i++){
        // printf("%d\n",a[i]+=a[i-1]);
        assert(abs(a[i]+=a[i-1])<=1);
    }
    // puts("");
    for(int i=1;i<=nn;i++)printf("%d ",c[i]);
    return 0;
}
  1. CF1810G The Maximum Prefix

\(\overset{\mathbf{Maximum\ Prefix}}{\text{妈克西马木普利非克斯}}\)这道题,你必须知道的是:状态中一定要有前缀和最大值,而顺序转移必须额外记录目前前缀和,因此一般倒推。\(f_{i,j}\) 表示后 \(i+1\) 位已确定、后 \(i+1\) 位的前缀和最大值为 \(j\) 的概率。转移 \(f_{i-1,j+1}\overset +\gets p_if_{i,j},f_{i-1,\max(0,j-1)}\overset +\gets (1-p_i)f_{i,j}\)。如果想求长度为 \(k\) 时的答案,你应该以 \(f_{k,0}=1\) 为起点来 dp。最后得到 \(ans_k=\sum f_{0,i}h_i\)。必须做 \(n\) 次。

这个算法为什么很残疾呢?dp 是允许求出单起点、多终点或单终点、多起点答案的。但以上算法钦定起点 \(f_{k,0}\) 又钦定终点 \(f_0\),蠢。

聪明一点的话,设 \(g_{i,j}\) 表示\(f_{i,j}\) 为起点再确定前 \(i\) 位的期望得分。终点仍然在 \(g_0\),有 \(g_{0,i}=h_i\)。转移 \(g_{i,j}=p_ig_{i-1,j+1}+(1-p_i)g_{i-1,\max(0,j-1)}\)。这样做就是单终点多起点的,\(k\) 的答案即 \(g_{k,0}\)

👍
#include<bits/stdc++.h>
#define LL long long
#define fi first
#define se second
using namespace std;

const int N=5e3+10,mod=1e9+7;
int n,p[N],f[N][N],T;

int qpow(int x,int y){
    int z=1;
    while(y){
        if(y&1)z=1llu*z*x%mod;
        y>>=1;x=1llu*x*x%mod;
    }
    return z;
}

signed main(){
    //freopen(".in","r",stdin);
    //freopen(".out","w",stdout);
    scanf("%d",&T);
    while(T--){
        scanf("%d",&n);
        for(int i=1;i<=n;i++){
            int x,y;scanf("%d%d",&x,&y);
            p[i]=1llu*x*qpow(y,mod-2)%mod;
        }
        for(int i=0;i<=n;i++)scanf("%d",&f[0][i]);
        for(int i=1;i<=n;i++){
            for(int j=0;j<=n;j++)
                f[i][j]=(1ll*p[i]*f[i-1][j+1]+1ll*(mod+1-p[i])*f[i-1][max(0,j-1)])%mod;
            printf("%d ",f[i][0]);
        }
        puts("");
    }
    return 0;
}
  1. CF1458C Latin Square

为方便取模,我们认为行、列、值均为 0-index。考虑一个位置的属性:位于 \((i,j)\),值为 \(a_{i,j}\)

以 U 为例,属性变为位于 \((i-1\bmod n,j)\),值为 \(a_{i,j}\)。以 I 为例,属性变为位于 \((i,a_{i,j})\),值为 \(j\)

这些变换如果想要高效地维护,必须允许合并,可以使用矩阵乘法。如果一个位置表示为向量 \([i,j,a_{i,j},1]\),容易对 6 种操作分别构造矩阵。

最后用初始向量乘上操作矩阵就可以迅速求出该点最后的属性。

👍
#include<bits/stdc++.h>
#define LL long long
#define fi first
#define se second
using namespace std;

const int N=1e3+10,M=1e5+10;
int n,m,a[N][N],b[N][N];
char op[M];
struct matrx{
    int a[4][4],x,y;
    const matrx operator*(const matrx &val)const{
        matrx ret;ret.x=x;ret.y=val.y;
        for(int i=0;i<ret.x;i++)
            for(int j=0;j<ret.y;j++){
                ret.a[i][j]=0;
                for(int k=0;k<y;k++)(ret.a[i][j]+=a[i][k]*val.a[k][j])%=n;
            }
        return ret;
    }
}G,R,L,D,U,I,C,F,H;

matrx E(){
    matrx ret;ret.x=ret.y=4;
    for(int i=0;i<4;i++)    
        for(int j=0;j<4;j++)ret.a[i][j]=(i==j);
    return ret;
}

void solve(){
    scanf("%d%d",&n,&m);
    for(int i=1;i<=n;i++)
        for(int j=1;j<=n;j++)scanf("%d",&a[i-1][j-1]),a[i-1][j-1]--;
    U.a[3][0]=n-1;
    L.a[3][1]=n-1;
    G=E();
    scanf("%s",op+1);
    for(int i=1;i<=m;i++){
        if(op[i]=='R')G=G*R;
        if(op[i]=='L')G=G*L;
        if(op[i]=='D')G=G*D;
        if(op[i]=='U')G=G*U;
        if(op[i]=='I')G=G*I;
        if(op[i]=='C')G=G*C;
    }
    for(int i=0;i<n;i++)
        for(int j=0;j<n;j++){
            F.a[0][0]=i;
            F.a[0][1]=j;
            F.a[0][2]=a[i][j];
            H=F*G;
            b[H.a[0][0]][H.a[0][1]]=H.a[0][2];
        }
    for(int i=0;i<n;i++,puts(""))
        for(int j=0;j<n;j++)printf("%d ",b[i][j]+1);
    puts("");
}

signed main(){
    G=R=L=D=U=I=C=E();
    D.a[3][0]=1;U=D;
    R.a[3][1]=1;L=R;
    I.a[2][1]=I.a[1][2]=1;I.a[1][1]=I.a[2][2]=0;
    C.a[2][0]=C.a[0][2]=1;C.a[0][0]=C.a[2][2]=0;
    F.x=1;F.y=4;F.a[0][3]=1;
    int T;scanf("%d",&T);
    while(T--)solve();
    return 0;
}

这道题还可以加强为取出一个操作区间执行后单点求值,用线段树/猫树维护区间矩阵积。

posted @ 2026-01-04 11:54  Sheez  阅读(41)  评论(1)    收藏  举报
要想见证尘封的秘密,就要承受最严厉的惩罚。