【杂题选讲】2023下半年合集

· · 个人记录

陽だまりの小部屋。

回忆。

深沉的单词啊,,。

时间不多了。

二分图没奇环,所以排列中没有长度为 3 的下降子序列。我们对于所有满足 a_i>a_j,i<jj 拎出来,那么这些 j 构成的子序列显然是单调上升的(否则就存在 a_{i_1}>a_{j_1}>a_{j_2}),对于剩下的也是形成了一个单调上升子序列。这个和题目要求显然是充要的。

那么 dp_{i,0/1} 表示其中一个子序列(哪个不重要)当前以 i 结尾且 a_i 符号为负/正,另外一个子序列末项的最大值。然后转移便枚举 dp_{i-1} 在哪个子序列中,以及是否取反,共 4 中情况。

对于输出我们发现题目要求字典序最小,那我们 dp 白做了重新改为从后往前dp,当前到 i,另一个子序列首项的最大值。转移同理。那么对于方案直接从前往后看能不能放负数,能放就放,注意时刻保证子序列的单调上升。O(n)

dp=-inf;
dp[n][0]=dp[n][1]=inf;
for(int i=n-1;i;i--){
    for(int j=0;j<2;j++){
        for(int k=0;k<2;k++){
            int x=a[i]*(j?1:-1),y=a[i+1]*(k?1:-1);
            if(x<y)dp[i][j]=max(dp[i][j],dp[i+1][k]);//同个子序列 
            if(x<dp[i+1][k])dp[i][j]=max(dp[i][j],y);//不同子序列 
        }
    }
    if(dp[i][0]==-inf&&dp[i][1]==-inf)NO;
}
YES;
A=B=-inf;//维护两个子序列当前的末项 
for(int i=1;i<=n;i++){
    if(-a[i]>A&&dp[i][0]>B)A=-a[i],out(-a[i]);
    else if(-a[i]>B&&dp[i][1]>A)B-=a[i],out(-a[i]);
    else{
        if(a[i]>A)A=a[i];else B=a[i];
        out(a[i]);
    }
    if(A<B)swap(A,B);
}

2023.7.4 白鸟。

必定存在一个区间其不动,情况必定不劣。我们定义该区间为不动点。剩余的区间相当于要么移到不动点左边要么右边。

考虑总代价:

\begin{aligned}Cost&=\sum(r_{p_i}-l_o+i\times len_{p_i})+\sum(r_o-l_{q_i}+i\times len_{q_i})\\&=\sum(r_{p_i}+i\times len_{p_i})+\sum(-l_{q_i}+i\times len_{q_i})+(-l_o\times |P|+r_o\times |Q|)\end{aligned}

其中 o 表示不动点,而 p,q 表示左边的区间从左到右、右边的区间从右到左。那么显然是区间长度越小的越靠近 o

dp_{i,j} 表示前 i 个区间中有 j 个区间放在左边的最小代价。可以枚举不动点然后 O(n^3) dp。

考虑 |P||Q| 的关系。考虑操作完后 o 移动,其每向左移动一次产生的代价为 |Q|+1-|P|。那么若 |P|\neq |Q|,则 o 必定会移动,与假设存在一个区间不动矛盾。故左右两边区间个数应是相同的。此时 o 的贡献便是 \frac{n-1}2\times len_o

所以在 dp 上增加一维 0/1 表示是否加入了不动点(即是否计算其贡献)。

对于 n 是偶数的情况加入区间 (0,0),在计算 o 的贡献时记得减去 l_o,也就是 (0,0) 会走到 l_o 的贡献。

```cpp #include<bits/stdc++.h> using namespace std; #define int long long const int N=5e3+4; const int inf=1e17; int n,ans=inf,dp[N][N][2],fl; struct node{ int l,r; }p[N]; inline bool cmp(node a,node b){ return a.r-a.l>b.r-b.l; } inline void Min(int&x,int y){ x>y?x=y:x; return; } signed main(){ scanf("%lld",&n); for(int i=1;i<=n;i++)scanf("%lld%lld",&p[i].l,&p[i].r); if(n%2==0)p[++n]={0,0},fl=1; for(int i=0;i<=n;i++)for(int j=0;j<=n;j++)dp[i][j][0]=dp[i][j][1]=inf; sort(p+1,p+n+1,cmp); dp[0][0][0]=0; for(int i=1;i<=n;i++){ int len=p[i].r-p[i].l,lim=min(i,(n-1)/2); for(int j=0;j<=lim;j++){ if(j)Min(dp[i][j][0],dp[i-1][j-1][0]+(j-1)*len+p[i].r); if(i-j-1>=0)Min(dp[i][j][0],dp[i-1][j][0]+(i-j-1)*len-p[i].l); if(j)Min(dp[i][j][1],dp[i-1][j-1][1]+(j-1)*len+p[i].r); if(i-j-1>=0)Min(dp[i][j][1],dp[i-1][j][1]+(i-2-j)*len-p[i].l); Min(dp[i][j][1],dp[i-1][j][0]+len*(n-1)/2+(fl?p[i].l:0)); } } printf("%lld\n",dp[n][(n-1)/2][1]); return 0; } ``` - CF1749F Distance to the Path - 给你一棵树,点有点权,初始为 $0$,两种操作: 1.询问点权 2.将所有到**从 $u$ 到 $v$ 的路径**的距离不超过 $k$ 的点权值加上 $val$。 $n,Q\le 2\times 10^5,k\le 20

更好的理解请看:https://www.luogu.com.cn/blog/310801/cf1749f-distance-to-the-path-ti-xie

考虑如果不是到一条路径的距离而是到一个点怎么做。

发现 k 很小。于是一个自然且朴素的想法就是做一个 f_{u,k} 维护 u 子树内距离点 u\le k 的所有点要加上多少,对于查询便暴力往上跳至多 20 次,加上他们对应到点 uf。发现这样对于 u 的祖先的情况不好处理,考虑容斥的思想,改为距离正好是 k 的所有点。对于距离 u\le k 的且在子树外的点考虑用祖先去覆盖。

那么先处理 f_{u,k},对于 f_{fa_u} 会发现如果是 f_{fa_u,k},能够覆盖到 uk-1 层儿子,这是好的;但是子树外会存在点 v 使 dis_{u,v}=k+1>k,但 v 却被打上标记。而对于 f_{fa_u,k-1} 能够避免外部点到 u 距离大于 k 的情况,却难以覆盖 uk-1 层儿子。

自然会想到容斥。但是容斥好烦。不要容斥。考虑“算2次,跳1次”的方法:处理 f_{u,k},f_{u,k-1},然后是 f_{fa_u,k-1},f_{fa_u,k-2}\dots,容易发现这样能够完美覆盖全部距离 u\le k 的节点。往上最多跳 k 次,跳到 rt 时记得将剩下的 f 全部处理。

回归本题在路径上的情况:某个点到达路径的最终点必然要么是从下往上,要么从上往下。前者可以在 u\to v 路径上任何一个节点 p 的子树中出现,后者必然是 lca(u,v) 的祖先的情况。

考虑一样的做法,先树链剖分,对于每个 k 建一棵线段树,修改操作时先修改 u\to v 上所有点的 f_{node,k},然后找到 lca 后往上执行“算2次,跳1次”的操作,复杂度为 O(q\log^2n+dq\log n)

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

#define int long long
#define ls p<<1
#define rs p<<1|1

const int N=2e5+6;

int n,Q,tot,head[N],dep[N],sz[N],son[N],top[N],idx,dfn[N],rev[N],tr[N<<2][22],tag[N<<2][22],fa[N];

struct edge{
    int to,nxt;
}e[N<<1];

inline void addedge(int u,int v){
    e[++tot].to=v;
    e[tot].nxt=head[u];
    head[u]=tot;
    return;
}

inline void dfs1(int u,int faa){
    fa[u]=faa,sz[u]=1,dep[u]=dep[faa]+1;
    for(int i=head[u];i;i=e[i].nxt){
        int v=e[i].to;
        if(v==faa)continue;
        dfs1(v,u);
        sz[u]+=sz[v];
        if(sz[v]>sz[son[u]])son[u]=v;
    }
    return;
}

inline void dfs2(int u,int tp){
    top[u]=tp,dfn[u]=++idx,rev[idx]=u;
    if(son[u])dfs2(son[u],tp);
    for(int i=head[u];i;i=e[i].nxt){
        int v=e[i].to;
        if(v==fa[u]||v==son[u])continue;
        dfs2(v,v);
    }
    return;
}

inline int lca(int u,int v){
    while(top[u]!=top[v]){
        if(dep[top[u]]<dep[top[v]])swap(u,v);
        u=fa[top[u]];
    }
    return dep[u]<dep[v]?u:v;
}

inline void pushup(int p,int k){
    tr[p][k]=tr[ls][k]+tr[rs][k];
    return;
}

inline void pushdown(int p,int l,int r,int k){
    if(!tag[p][k])return;
    int mid=(l+r)>>1;
    tr[ls][k]+=tag[p][k]*(mid-l+1),tr[rs][k]+=tag[p][k]*(r-mid);
    tag[ls][k]+=tag[p][k],tag[rs][k]+=tag[p][k],tag[p][k]=0;
    return;
}

inline void update(int p,int l,int r,int s,int t,int k,int val){
    if(s<=l&&r<=t){
        tr[p][k]+=val*(r-l+1),tag[p][k]+=val;
        return;
    }
    pushdown(p,l,r,k);
    int mid=(l+r)>>1;
    if(s<=mid)update(ls,l,mid,s,t,k,val);
    if(t>mid)update(rs,mid+1,r,s,t,k,val);
    pushup(p,k);
    return;
}

inline int query(int p,int l,int r,int ps,int k){
    if(l==r)return tr[p][k];
    int mid=(l+r)>>1;
    pushdown(p,l,r,k);
    if(ps<=mid)return query(ls,l,mid,ps,k);
    else return query(rs,mid+1,r,ps,k);
}

inline void upd(int u,int v,int k,int val){
    if(k<0)return;
    while(top[u]!=top[v]){
        if(dep[top[u]]<dep[top[v]])swap(u,v);
        update(1,1,n,dfn[top[u]],dfn[u],k,val);
        u=fa[top[u]];
    }
    if(dep[u]<dep[v])swap(u,v);
    update(1,1,n,dfn[v],dfn[u],k,val);
    return;
}

signed main(){
    scanf("%lld",&n);
    for(int i=1;i<n;i++){
        int u,v;
        scanf("%lld%lld",&u,&v);
        addedge(u,v),addedge(v,u);
    }
    dfs1(1,0),dfs2(1,1);
    scanf("%lld",&Q);
    while(Q--){
        int op,u,v,k,val,ans=0,o;
        scanf("%lld",&op);
        if(op==1){
            scanf("%lld",&u);
            for(int i=0;i<=20;i++){
                ans+=query(1,1,n,dfn[u],i);
                u=fa[u];
                if(!u)break;
            }
            printf("%lld\n",ans);
        }
        else{
            scanf("%lld%lld%lld%lld",&u,&v,&val,&k),o=lca(u,v);
            upd(u,v,k,val),upd(o,o,k-1,val);
            for(int i=k-1;i>=0;i--){
                if(o==1){
                    for(int j=i-1;j>=0;j--)upd(o,o,j,val);
                    break;
                }
                else o=fa[o],upd(o,o,i,val),upd(o,o,i-1,val);
            }
        }
    }
    return 0;
}

对于每个 a_i=x 建立一棵线段树,那么修改操作相当于将第 x 棵树上的 [l,r] 区间删除后粘贴到第 x+1 棵树上。查询时 [l,r] 会在很多棵树上出现,时间复杂度就寄了。不会做啊!怎么办!

改为对于 a_i\ge x 建立线段树。那么每次修改只需要将 [l,r] 复制到第 x+1 棵树。那么 [l,r] 会转为出现在一段前缀上。我会二分!O(n\log^2n)

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

const int N=5e5+6;
const int M=1e7+8;

int n,m,Q,tr[M],ls[M],rs[M],rt[N],idx;

inline void pushup(int p){
    tr[p]=tr[ls[p]]+tr[rs[p]];
    return;
}

inline void build(int&p,int l,int r){
    if(!p)p=++idx;
    if(l==r){
        tr[p]=1;
        return;
    }
    int mid=(l+r)>>1;
    build(ls[p],l,mid),build(rs[p],mid+1,r);
    pushup(p);
    return;
}

inline void copy(int&p,int q,int l,int r,int s,int t){
    if(!q)return;
    if(s<=l&&r<=t){
        p=q;
        return;
    }
    else idx++;
    int mid=(l+r)>>1;
    ls[idx]=ls[p],rs[idx]=rs[p],tr[idx]=tr[p],p=idx;
    if(s<=mid)copy(ls[p],ls[q],l,mid,s,t);
    if(t>mid)copy(rs[p],rs[q],mid+1,r,s,t);
    pushup(p);
    return;
}

inline int query(int p,int l,int r,int s,int t){
    if(s<=l&&r<=t)return tr[p];
    int mid=(l+r)>>1,res=0;
    if(s<=mid)res+=query(ls[p],l,mid,s,t);
    if(t>mid)res+=query(rs[p],mid+1,r,s,t);
    return res;
}

int main(){
    scanf("%d%d",&n,&Q);
    build(rt[0],1,n);
    for(int i=1;i<=Q;i++){
        int op,l,r,x=0;
        scanf("%d%d%d",&op,&l,&r);
        if(op==1){
            scanf("%d",&x);
            copy(rt[x+1],rt[x],1,n,l,r);
        }
        else{
            int L=0,R=i;
            while(L<=R){
                int mid=(L+R)>>1;
                if(query(rt[mid],1,n,l,r))L=mid+1,x=mid;
                else R=mid-1;
            }
            printf("%d\n",x);
        }
    }
    return 0;
}

没有脑子选手会转化为一个长度为 k 的不降序列,和为 k,不出现连续 m 个相同的数。

一种常规的做法是差分来去除“不降”这个限制,令 b_i=a_i-a_{i-1},有 \sum b_i(k-i+1)=n,由于 b 之间没有位置顺序限制所以 reverse 一下,变为 \sum ib_i=n。设 f_{i,j} 表示当前在 b_i,和为 j

f_{i,j}=\sum_{p=i-m+1}^{i-1}\sum_{B=1}^{j/i} f_{p,j-iB}

其中 p 为枚举上一个不同的数的位置,B 即枚举 b_iB 是调和级数,可以前缀和优化至 O(n^2\ln n)

没脑子选手不会 dp 怎么办?发现题目去掉 m 的限制就是分拆数。而对于 m 的限制相当于数列前 m+1 项都是 1,方案数也就是 p_{i-j,j-(m+1)}。所以和分拆数一样 dp 就完事了。O(n^2)

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

#define int long long

const int N=5e3+4;
const int mod=998244353;

int n,m,dp[N][N],sdp[N][N];

signed main(){
    scanf("%d%d",&n,&m);
    dp[0][0]=sdp[0][0]=1;
    for(int i=1;i<=n;i++){
        for(int j=1;j<=n;j++){
            for(int k=i;k<=j;k+=i){
                dp[i][j]=(dp[i][j]+sdp[i-1][j-k])%mod;
                if(i>m)dp[i][j]=(dp[i][j]-sdp[i-m-1][j-k]+mod)%mod;
            }
            sdp[i][j]=(sdp[i-1][j]+dp[i][j])%mod;
        }
        sdp[i][0]=1;
    }
    for(int i=1;i<=n;i++)printf("%d\n",dp[i][n]);
    return 0;
}

boring。将 m 次操作与长度为 n 的序列合起来变为 (m+1)\times n 的矩阵 M,其中 M_{i,j} 表示第 i 次操作选择了第 j 个位置加上 v。每一行都是形如一段前缀 0 以后接着一段后缀 v。总期望就是 \prod \sum M_{i,j},也就是每一列的和的乘积。那这玩意相当于一个 (t_1+t_2+...)(t'_1+t'_2+...)(t''_1+t''_2+...)... 的一个东西,直接拆贡献就是第 i 个位置在每次操作时选择 0/v/a_i。选择 0 贡献就没了就不管,也就是每次要么选 v 要么就是 a_i

f_{i,j} 表示 [1,i] 选择好了且被这些元素选择的行共 j 行。转移枚举 i+1 的决策:

  1. 选择 a_{i+1}f_{i,j}\times a_{i+1}\to f_{i+1,j}
  2. 选择之前选过的 j 行中的 vf_{i,j}\times jv\to f_{i+1,j}
  3. 选择不在 j 行中的 vf_{i,j}\times (m-j)v\to f_{i+1,j+1}

总期望即为 \dfrac{\sum f_{n,j}n^{m-j}}{n^m}O(n^2)

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

#define int long long

const int mod=1e9+7;
const int N=5e3+4;

int n,m,v,ans,dp[N][N],a[N];

inline int qpow(int a,int b){
    int res=1;
    while(b){
        if(b&1)res=res*a%mod;
        a=a*a%mod;
        b>>=1;
    }
    return res;
}

signed main(){
    scanf("%lld%lld%lld",&n,&m,&v),dp[0][0]=1;
    for(int i=1;i<=n;i++){
        scanf("%lld",&a[i]);
        for(int j=0;j<=min(n,m);j++){
            dp[i][j]=(dp[i][j]+dp[i-1][j]*(a[i]+j*v%mod)%mod)%mod;
            dp[i][j+1]=(dp[i][j+1]+dp[i-1][j]*(m-j)%mod*i%mod*v%mod)%mod;
        }
    }
    for(int i=0;i<=min(n,m);i++)ans=(ans+dp[n][i]*qpow(n,m-i)%mod)%mod;
    printf("%lld",ans*qpow(qpow(n,m),mod-2)%mod);
    return 0;
}

挺nb的。若 [i,i+2],[i+2,i+4] 不合法,那么 [i+1,i+3] 也不合法。看起来很蠢对吧,但是就是这么蠢,容斥有 s 个段 [i,i+2] 不合法,那么 [i+1,i+3] 必须合法。做完了!O(n)

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

#define int long long

const int mod=998244353;
const int N=3e6+7;

int n,k,A,B,C,fac[N],ifac[N],pw[N],ans;

inline int qpow(int a,int b){
    int res=1;
    while(b){
        if(b&1)res=res*a%mod;
        a=a*a%mod;
        b>>=1;
    }
    return res;
}

inline void init(int n){
    fac[0]=ifac[0]=pw[0]=1;
    for(int i=1;i<=n;i++)fac[i]=fac[i-1]*i%mod,pw[i]=pw[i-1]*2%mod;
    ifac[n]=qpow(fac[n],mod-2);
    for(int i=n-1;i;i--)ifac[i]=ifac[i+1]*(i+1)%mod;
    return;
} 

inline int c(int n,int m){
    if(n<m||n<0||m<0)return 0;
    return fac[n]*ifac[m]%mod*ifac[n-m]%mod;
}

signed main(){
    scanf("%lld%lld%lld",&A,&B,&C),n=A+B+C,k=min(A,min(B,C));
    init(n),ans=c(n,A)*c(n-A,B)%mod;
    for(int i=1;i<=k;i++){
        int m=n-3*i,t=c(m,A-i)*c(m-(A-i),B-i)%mod;
        if(i&1)ans=(ans-(t*c(m+i,i)%mod*pw[i]%mod+t*c(m+i-1,i-1)%mod*pw[i-1]%mod)%mod+mod)%mod;
        else ans=(ans+(t*c(m+i,i)%mod*pw[i]%mod+t*c(m+i-1,i-1)%mod*pw[i-1]%mod)%mod)%mod;
    }
    printf("%lld",ans);
    return 0;
}

pbyyds。

相信大家都会 O(n^2)。我们发现从小到大插入 i 时产生新的逆序对个数 x_i\in[0,i-1] 且每种情况唯一。那么题目就是求 \sum x_i=k 的方案数了,其中 x_i\in[0,i-1]

相信大家都会容斥。将其转化为

\sum_{S}(-1)^{|S|}R(k-\sum_{i\in S}i)

其中 S 为钦定的不满足条件的 i 的集合,那么对于 i\in Sx_i\ge i,x_i-i\ge0。而不属于 S 的限制为 x_i\ge 0。定义 \forall i\in S ,y_i=x_i-i,那么属于 Sy_i 和不属于 Sx_i 共同限制都是 \ge 0

所以 R(j) 含义即为 (\sum_{i=1}^nz_i)=j 的方案数,其中 z_i\ge0。那么这个大家都会插板法,所以问题转化为如何快速求出 \sum_{i\in S}i

考虑设计dp:f_{i.j} 表示从 1\sim n 中选择 i 和不同的数且和为 j 的方案数。

不妨令此时我们选择了 a_1<a_2<\dots <a_i,那么令 x 轴为下标、y 轴为 a_I 的值,那么 j 即为一个形如从左下至右上增长的一个阶梯形的面积。

考虑转移。为了使转移不重不漏,我们钦定只能向下/左转移。我们可以给 a 集体加一,那么相当于在阶梯下方垫高一层,转移为 f_{i,j}\to f_{i,j+i}。此时显然 a 仍满足两两不同。

考虑对于 i 这维的转移。在垫高一层后考虑在最左侧增加一个高度为 1 的列,此时便形如 1,a_1,a_2,\dots, a_i,仍满足两两不同。转移为 f_{i,j}\to f_{i+1,j+i+1}

注意到最右侧的一列高度可能会大于 n,那么减去这一列即为不合法的方案,是 f_{i-1,j-n-1}

注意 j 范围 \le k,而 j 为阶梯面积,故 i 范围是 O(\sqrt k) 的。总时间复杂度 O(k\sqrt k)

#pragma GCC optimize("Ofast")
#include<bits/stdc++.h>
using namespace std;

#define int long long

const int N=1e5+6;
const int M=470;
const int mod=1e9+7;

int n,k,dp[M][N],fac[N<<1],ifac[N<<1],ans;

inline int qpow(int a,int b){
    int res=1;
    while(b){
        if(b&1)res=res*a%mod;
        a=a*a%mod;
        b>>=1;
    }
    return res;
}

inline void init(int n){
    fac[0]=ifac[0]=1;
    for(int i=1;i<=n;i++)fac[i]=fac[i-1]*i%mod;
    ifac[n]=qpow(fac[n],mod-2);
    for(int i=n-1;i;i--)ifac[i]=ifac[i+1]*(i+1)%mod;
    return;
}

inline int C(int n,int m){
    if(n<m||n<0||m<0)return 0;
    return fac[n]*ifac[m]%mod*ifac[n-m]%mod;
}

signed main(){
    scanf("%lld%lld",&n,&k),init(N*2-1);
    dp[0][0]=1;
    for(int i=1;i<=453;i++)for(int j=1;j<=k;j++){
        if(j>=i)dp[i][j]=(dp[i][j-i]+dp[i-1][j-i])%mod;
        if(j>n)dp[i][j]=(dp[i][j]-dp[i-1][j-n-1]+mod)%mod;
    }
    for(int i=0;i<=453;i++)for(int j=0;j<=k;j++)ans=(ans+(i&1?mod-1:1)*dp[i][j]%mod*C(k-j+n-1,n-1)%mod)%mod;
    printf("%lld",ans);
    return 0;
}

连通块个数=分界点/2。定义分界点为 a,b 之间表示 a\ge x,b<x 或反之。

使用树状数组维护分界点个数,对于 a\ge x 时加一,b>x 时减一。

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

const int N=2e5+6;

int n,m,lsh[N<<1],a[N],tot,tr[N<<2];
struct node{int op,x,y;}q[N];

inline int lowbit(int x){return x&-x;}
inline void add(int x,int val){while(x<=tot)tr[x]+=val,x+=lowbit(x);}
inline int qry(int x){int res=0;while(x)res+=tr[x],x-=lowbit(x);return res;}

inline void upd(int x,int op){
    int l=a[x-1],r=a[x];
    if(l>r)swap(l,r);l++;
    add(l,op),add(r+1,-op);
    return;
}

int main(){
    scanf("%d%d",&n,&m),lsh[++tot]=0;
    for(int i=1;i<=n;i++)scanf("%d",&a[i]),lsh[++tot]=a[i];
    for(int i=1;i<=m;i++){
        scanf("%d",&q[i].op);
        if(q[i].op==1)scanf("%d",&q[i].x),lsh[++tot]=q[i].x;
        else scanf("%d%d",&q[i].x,&q[i].y),lsh[++tot]=q[i].y;
    }
    sort(lsh+1,lsh+tot+1),tot=unique(lsh+1,lsh+tot+1)-lsh-1;
    for(int i=0;i<=n+1;i++)a[i]=lower_bound(lsh+1,lsh+tot+1,a[i])-lsh;
    for(int i=1;i<=n+1;i++)upd(i,1);
    for(int i=1;i<=m;i++){
        if(q[i].op==1){
            q[i].x=lower_bound(lsh+1,lsh+tot+1,q[i].x)-lsh;
            printf("%d\n",qry(q[i].x)/2);
        }
        else{
            q[i].y=lower_bound(lsh+1,lsh+tot+1,q[i].y)-lsh;
            upd(q[i].x,-1),upd(q[i].x+1,-1),a[q[i].x]=q[i].y;
            upd(q[i].x,1),upd(q[i].x+1,1);
        }
    }
    return 0;
}

出现奇数次和可以出现 0 次看似有点冲突,将将题目改成出现次数为偶数次。

一个经典的trick:使用异或和来判断奇偶,给每一个区间随机一个权值即颜色,则当选择的区间中所有点的异或和为零时,每种颜色都出现偶数次,再使用前缀异或和即可维护。

对于把奇数个转为偶数个,将每个区间(包括自己选择的好区间)左侧或右侧 +/-1 即可。

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

#define int long long

mt19937_64 rnd(time(0));

const int N=1e5+6;

int n,m,a[N],t[N],ans,c;
map<int,int>cnt,mps;

signed main(){
    scanf("%lld%lld",&n,&m);
    for(int i=1;i<=n;i++){
        int x=rnd(),l,r;
        scanf("%lld%lld",&l,&r),l++;
        a[l]^=x,a[r+1]^=x;
        t[l-1]++,t[r+1]--;
    }
    for(int i=1;i<=m;i++){
        a[i+2]^=a[i],t[i]+=t[i-1],c++;
        if(t[i])c=0;
        cnt[a[i]]++,mps[a[i]]+=i-1;
        ans+=cnt[a[i]]*i-mps[a[i]]-c*(c+1)/2;
    }
    printf("%lld\n",ans);
}

先考虑朴素地分解质因数,所有次数对 3 取模,得到 a_i=\prod p_i^{k_i},有 b_i=\prod p_i^{3-k_i},则 a_ib_i 为完全立方数。所以将每个数先转为次数 <3 的模式后对于每一对 a_ib_i 看哪个出现的多即可。

但是不能直接分解,但是可以分解 \sqrt[3] V 内的质因数。考虑剩下的那个数 d_i,此时仅有三种可能:p,pq,p^2,分类一下即可。

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

#define ll long long

const int N=1e5+6;
const int M=330;

int n,p[N],pid,cnt[N][M],ans;
bool vis[N],cubic[N];
ll a[N],b[N],d[N],mx;
map<ll,int>mps;
queue<ll>q;

void init(int n){
    for(int i=2;i<=n;i++){
        if(!vis[i])p[++pid]=i;
        for(int j=1;j<=pid&&i*p[j]<=n;j++){
            vis[i*p[j]]=1;
            if(i%p[j]==0)break;
        }
    }
    return;
}

inline bool isq(ll x){
    ll y=sqrt(x);
    return y*y==x;
}

signed main(){
    scanf("%d",&n);
    for(int i=1;i<=n;i++)scanf("%lld",&a[i]),mx=max(mx,a[i]);
    int A=pow(mx,1./3),B=sqrt(mx);
    init(B);
    for(int i=1;i<=n;i++){
        int tmp=1;
        for(int j=1;j<=pid&&p[j]<=A;j++){
            while(a[i]%p[j]==0)a[i]/=p[j],cnt[i][j]++;
            cnt[i][j]%=3;
            if(cnt[i][j])tmp*=p[j];
            if(cnt[i][j]==2)tmp*=p[j];
        }d[i]=a[i],a[i]*=tmp;
        if(a[i]==1)ans=1,cubic[i]=1;
    }
    for(int i=1;i<=n;i++)if(!cubic[i]){
        if(d[i]==1||(d[i]<=B&&!vis[d[i]])||isq(d[i]))
            q.push(i),mps[a[i]]++;
        else ans++;
    }
    while(!q.empty()){
        int i=q.front();q.pop();
        if(isq(d[i]))b[i]=sqrt(d[i]);
        else b[i]=d[i]*d[i];
        for(int j=1;j<=pid&&p[j]<=A;j++){
            if(cnt[i][j])b[i]*=p[j];
            if(cnt[i][j]==1)b[i]*=p[j];
        }
        ans+=max(mps[a[i]],mps[b[i]]),mps[a[i]]=mps[b[i]]=0;
    }
    printf("%d\n",ans);
    return 0;
}

最朴素的暴力即 f_{u,0/1} 表示选或不选,暴力枚举权值可能,O(nk^n)。注意到 f 值域为 O(nk) 的,所以可以 dp 套 dp,dp_{u,x,y} 表示 f_{u,0}=x,f_{u,1}=y 时的情况。O(n^4k^4)

注意到瓶颈在于 dp 状态数就寄了,改变 f 的定义为是否钦定 u 不选,那么有 f_{u,0}-f_{u,1}\le a_u\le k,所以有 dp_{u,x,d},其中 f_{u,0} 被表示为 x+dO(n^2k^4)

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

#define int long long

const int N=1e3+4;
const int M=5*N;
const int mod=1e9+7;

int n,k,sz[N],f[N][M][6],g[M][6],tot,head[N];

struct edge{
    int to,nxt;
}e[N<<1];

inline void addedge(int u,int v){
    e[++tot].to=v;
    e[tot].nxt=head[u];
    head[u]=tot;
    return;
}

void dfs(int u,int fa){
    sz[u]=1;
    for(int i=1;i<=k;i++)f[u][0][i]=1;
    for(int i=head[u];i;i=e[i].nxt){
        int v=e[i].to;
        if(v==fa)continue;
        dfs(v,u);
        memset(g,0,sizeof(g));
        for(int i=0;i<=k*sz[u];i++)
        for(int j=0;j<=k;j++)if(f[u][i][j])
        for(int p=0;p<=k*sz[v];p++)
        for(int q=0;q<=k;q++)if(f[v][p][q])
        (g[i+p+q][max(i+j+p,i+p+q)-(i+p+q)]+=f[u][i][j]*f[v][p][q]%mod)%=mod;
        memcpy(f[u],g,sizeof(g)),sz[u]+=sz[v];
    }
    return;
}

signed main(){
    scanf("%lld%lld",&n,&k);
    for(int i=1;i<n;i++){
        int u,v;
        scanf("%lld%lld",&u,&v);
        addedge(u,v),addedge(v,u);
    }
    dfs(1,0);
    for(int i=1;i<=n*k;i++){
        int ans=0;
        for(int d=0;d<=min(i,k);d++)ans=(ans+f[1][i-d][d])%mod;
        printf("%lld\n",ans);
    }
    return 0;
}

先将 x[i]-=0.5,变为 x_i+x_j\le 0,x_i\in[-0.5,0.5]。将 x_i 按照绝对值大小排序后发现 x_i+x_j\le 0 等价于 x_j\le 0(钦定 |x_i|\le |x_j|),大于等于同理,所以按照绝对值排序后给每个数安排正负号,那么转为状压 dp,做完了。

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

#define int long long

const int N=21;
const int mod=998244353;
const int inv2=(mod+1)/2;

int n,m,g[2][N],dp[1<<N],ans;

inline int qpow(int a,int b){
    int res=1;
    while(b){
        if(b&1)res=res*a%mod;
        a=a*a%mod;
        b>>=1;
    }
    return res;
}

signed main(){
    scanf("%lld%lld",&n,&m);
    for(int i=1;i<=m;i++){
        int op,x,y;
        scanf("%lld%lld%lld",&op,&x,&y),x--,y--;
        g[op][x]|=(1<<y),g[op][y]|=(1<<x);
    }dp[0]=1;
    for(int s=1;s<(1<<n);s++)for(int i=0;i<n;i++)if(s&(1<<i)){
        if(!(g[0][i]&s))dp[s]=(dp[s]+dp[s^(1<<i)])%mod;
        if(!(g[1][i]&s))dp[s]=(dp[s]+dp[s^(1<<i)])%mod;
    }
    int ans=dp[(1<<n)-1];
    for(int i=1;i<=n;i++)ans=ans*qpow(i,mod-2)%mod;
    printf("%lld",ans*qpow(inv2,n)%mod);
    return 0;
}

大胆猜想答案 \le 2。对于 m=0 时答案为 1 否则为 2。怎么证明?

C=\sum_{(u,v)\in E}|col_u-col_v|,每次从图中随便拿一个不满足条件的点,即超过⼀半的邻点的颜⾊与它不同,将其反色,发现 C 至少 +1。那么 S 单增且有显然上界 m,故必定会终止即得证,且最多 m 轮,每轮容易通过记录每个点周围颜色与其是否相同的值之和做到 O(n),总时间复杂度 O(nm)

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

const int N=5e3+4;

int n,m,a[N],col[N];
queue<int>q;
vector<int>g[N];

int main(){
    scanf("%d%d",&n,&m);
    for(int i=1;i<=m;i++){
        int u,v;
        scanf("%d%d",&u,&v);
        g[u].push_back(v),g[v].push_back(u);
        a[u]--,a[v]--;
    }
    if(!m){
        puts("1");
        for(int i=1;i<=n;i++)printf("1 ");
        return 0;
    }
    else puts("2");
    for(int i=1;i<=n;i++)q.push(i);
    while(!q.empty()){
        int u=q.front();q.pop();
        if(a[u]>=0)continue;
        col[u]^=1,a[u]*=-1;
        for(int v:g[u]){
            a[v]+=(col[u]!=col[v]?1:-1)*2;
            if(a[v]<0)q.push(v);
        }
    }
    for(int i=1;i<=n;i++)printf("%d ",col[i]+1);
    return 0;
}

这两题对于我的做法而言没有区别,所以放在一起讲。

先把 trie 树建出来(实际上不需要真正建出),异或上 x 相当于将 x 拆成二的幂次的和后每次异或一个二次幂。在 trie 上异或 2^k 是好做的,相当于将从下往上(从 0 标号)的第 k 层两两相邻区间交换。

在每个节点上维护题目需要的信息,以 CF1553H 为例,我们维护该节点表示的区间的答案、最大值与最小值,那么节点的答案可以被左子树的最大值与右子树最小值的差值来转移。

注意对于某一层有影响的只有它下面的层,所以考虑这样的一个dp:

对于P9595 则更简单些,维护以左端点为起点和以右端点为终点的极长连续段即可。 ```cpp //CF1553H #include<bits/stdc++.h> using namespace std; const int inf=1e9; int n,K; vector<vector<int>>f[21],mn[21],mx[21]; int main(){ scanf("%d%d",&n,&K); for(int i=0;i<=K;i++){ mn[i].resize(1<<i),mx[i].resize(1<<i),f[i].resize(1<<i); for(int j=0;j<(1<<i);j++)mn[i][j].resize(1<<K-i),mx[i][j].resize(1<<K-i),f[i][j].resize(1<<K-i); } for(int i=0;i<(1<<K);i++)f[0][0][i]=mn[0][0][i]=inf,mx[0][0][i]=-inf; for(int i=1;i<=n;i++){ int x;scanf("%d",&x); mn[0][0][x]=mx[0][0][x]=x; } for(int i=1;i<=K;i++){ for(int j=0;j<(1<<i-1);j++){ for(int k=0;k<(1<<K-i);k++){ int J=j|(1<<i-1); f[i][j][k]=min(min(f[i-1][j][k<<1],f[i-1][j][k<<1|1]),mn[i-1][j][k<<1|1]-mx[i-1][j][k<<1]); f[i][J][k]=min(min(f[i-1][j][k<<1],f[i-1][j][k<<1|1]),mn[i-1][j][k<<1]-mx[i-1][j][k<<1|1]+(1<<i)); mn[i][j][k]=min(mn[i-1][j][k<<1],mn[i-1][j][k<<1|1]); mn[i][J][k]=min(mn[i-1][j][k<<1|1]-(1<<i-1),mn[i-1][j][k<<1]+(1<<i-1)); mx[i][j][k]=max(mx[i-1][j][k<<1|1],mx[i-1][j][k<<1]); mx[i][J][k]=max(mx[i-1][j][k<<1]+(1<<i-1),mx[i-1][j][k<<1|1]-(1<<i-1)); } } } for(int i=0;i<(1<<K);i++)printf("%d ",f[K][i][0]); return 0; } //P9595 #include<bits/stdc++.h> using namespace std; const int _=19; int n,Q,now; vector<vector<int>>f[21],l[21],r[21]; int main(){ scanf("%d%d",&n,&Q); for(int i=0;i<=_;i++){ l[i].resize(1<<i),r[i].resize(1<<i),f[i].resize(1<<i); for(int j=0;j<(1<<i);j++)l[i][j].resize(1<<_-i),r[i][j].resize(1<<_-i),f[i][j].resize(1<<_-i); } for(int i=1;i<=n;i++){ int x;scanf("%d",&x); l[0][0][x]=r[0][0][x]=f[0][0][x]=1; } for(int i=1;i<=_;i++){ for(int j=0;j<(1<<i-1);j++){ for(int k=0;k<(1<<_-i);k++){ int J=j|(1<<i-1); f[i][j][k]=max(max(f[i-1][j][k<<1],f[i-1][j][k<<1|1]),l[i-1][j][k<<1|1]+r[i-1][j][k<<1]); f[i][J][k]=max(max(f[i-1][j][k<<1],f[i-1][j][k<<1|1]),l[i-1][j][k<<1]+r[i-1][j][k<<1|1]); l[i][j][k]=max(l[i-1][j][k<<1],l[i-1][j][k<<1]==(1<<i-1)?l[i-1][j][k<<1]+l[i-1][j][k<<1|1]:0); l[i][J][k]=max(l[i-1][j][k<<1|1],l[i-1][j][k<<1|1]==(1<<i-1)?l[i-1][j][k<<1|1]+l[i-1][j][k<<1]:0); r[i][j][k]=max(r[i-1][j][k<<1|1],r[i-1][j][k<<1|1]==(1<<i-1)?r[i-1][j][k<<1|1]+r[i-1][j][k<<1]:0); r[i][J][k]=max(r[i-1][j][k<<1],r[i-1][j][k<<1]==(1<<i-1)?r[i-1][j][k<<1]+r[i-1][j][k<<1|1]:0); } } } while(Q--){ int x;scanf("%d",&x),now^=x; printf("%d\n",f[_][now][0]); } } ``` - CF1149D Abandoning Roads - 一张 $n$ 个点 $m$ 条边的无向图,只有 $a,b$ 两种边权($a<b$),对于每个 $i$,求图中所有的最小生成树中,从 $1$ 到 $i$ 距离的最小值。$n\le 70,m\le 200$。 考虑建出最小生成树的过程,显然是先选择完 $a$ 边后才会考虑 $b$ 边,那么在选择 $b$ 边之前显然这个图被分为了若干个连通块,块内两两之间都是 $a$ 边,后文的“连通块”均代指此处的 $a$ 边连通块,通过 dfs 给连通块染色是容易的。 对于一条可能在最小生成树上出现的路径,显然它不会离开一个连通块后又回来(因为是树没有环),所以一个朴素的 dp 就是 $dp_{S,u}$ 表示已经遍历过了 $S$ 这个集合内的连通块,当前在点 $u$,转移便是最短路问题,放在 dijkstra 上跑可以获得 $O(2^n(n+m)\log2^n(n+m))$ 的时间复杂度。 瓶颈在于 $2^n$ 上,而 $n\le 70$ 启发我们状压,如何压缩状态?注意到对于大小 $\le 3$ 的连通块由于内部**最长路径也只有 $2a$**,而 $2a<2b$,故在最短路的流程中**不会**回到此连通块。所以这种连通块的状态是没有用的,故只记录大小 $>3$ 的连通块即可,时间复杂度为 $O(2^{\frac n 4}(n+m)\log2^{\frac n 4}(n+m))$。 ```cpp #include<bits/stdc++.h> using namespace std; #define ppi pair<int,pair<int,int>> #define mp make_pair #define fi first #define se second const int N=72; const int M=220; const int P=(1<<18)+1; int n,m,tot,cnt,a,b,head[N],col[N],idx[2],vis[N],typ[N],dp[P][N]; struct edge{ int to,nxt,w; }e[M<<1]; inline void addedge(int u,int v,int w){ e[++tot].to=v; e[tot].nxt=head[u]; head[u]=tot; e[tot].w=w; return; } void dfs(int u){ cnt++,vis[u]=1; for(int i=head[u];i;i=e[i].nxt){ int v=e[i].to; if(vis[v]||e[i].w==b)continue; dfs(v); } return; } void Dfs(int u,bool o){ col[u]=idx[o],typ[u]=o; for(int i=head[u];i;i=e[i].nxt){ int v=e[i].to; if(col[v]||e[i].w==b)continue; Dfs(v,o); } return; } void Dijkstra(){ priority_queue<ppi,vector<ppi>,greater<ppi>>q; memset(dp,0x3f,sizeof(dp)),dp[0][1]=0; q.push(mp(0,mp(0,1))); while(!q.empty()){ int d=q.top().fi,u=q.top().se.se,S=q.top().se.fi;q.pop(); if(dp[S][u]<d)continue; for(int i=head[u];i;i=e[i].nxt){ int v=e[i].to; if(col[u]==col[v]&&e[i].w==b)continue; if(typ[v]&&(S&(1<<col[v]-1)))continue; int T=S; if(typ[u]&&col[u]!=col[v])T|=(1<<col[u]-1); if(dp[T][v]>d+e[i].w)dp[T][v]=d+e[i].w,q.push(mp(dp[T][v],mp(T,v))); } } return; } int main(){ scanf("%d%d%d%d",&n,&m,&a,&b),idx[0]=n; for(int i=1;i<=m;i++){ int u,v,w; scanf("%d%d%d",&u,&v,&w); addedge(u,v,w),addedge(v,u,w); } for(int i=1;i<=n;i++)if(!vis[i]){ cnt=0,dfs(i); if(cnt>3)idx[1]++,Dfs(i,1); else idx[0]++,Dfs(i,0); } Dijkstra(); for(int i=1;i<=n;i++){ int ans=dp[0][i]; for(int s=1;s<(1<<18);s++)ans=min(ans,dp[s][i]); printf("%d ",ans); } return 0; } ``` - P7450 [THUSCH2017] 巧克力 - 给定一个 $n × m$ 的表格,每个格子有一个权值 $a_{i,j}$ 和颜色 $c_{i,j}$。$c_{i,j}=-1$ 表示这个格子不可用。问最小的四连通块大小,使得该连通块内没有不可用的格子,且包含至少 $k$ 种不同的颜色。在大小最小的前提下,求出连通块内权值中位数的最小值。$n\times m\le 233,k\le 5$。 我们先考虑假如所有点的颜色 $\in[0,k)$ 的情况。那么这就是斯坦纳树板子。至于中位数,一个经典的想法,便是二分中位数,然后 $\le mid$ 的点权值设为 $\inf-1$,大于的设为 $\inf+1$,那么就容易保证大小最小这一限制的优先级在前面,判断即将权值和对 $\inf$ 取模即可,此题中 $\inf$ 由于 $n,m$ 范围极小故开 $1000$ 就好了。 现在颜色的取值范围 $C\ge k$ 的情况,一个极其暴力的想法是枚举 $\binom C k$ 种方案后跑。组合数太大了显然弃掉这个想法。注意到我们上一段中提及的做法时间复杂度非常充裕,$O(nm3^k\log nm)$ 在 $5s$ 的时限里可以重复跑好多次,这启示我们**随机化**。 将每种颜色随机映射到 $[0,k)$ 中。考虑最优解的情况下,选择的颜色必然是恰好 $k$ 种不同颜色。那么在新的图(颜色映射后的图)中我们得到的大小要最小,那么考虑我们最终选择的 $k$ 中颜色,它们必然映射到恰好 $k$ 个不同的新颜色上。否则我们必定不优。所以随机一次的成功概率是 $P=\dfrac{k!}{k^k}$ 的。我们如果随机 $T$ 次,失败概率就是 $P'=(1-P)^T$,当 $T=100$ 时 $P'\le 2\%$,保险起见并且由于时限宽裕 $T$ 开到 $200$。~~否则就会和我一样获得在loj上99分的好成绩。~~ 事实上这个随机颜色的方式即 Color-Coding 还是较为常见的。~~不出意外的话~~我会去专门为这个 [trick](https://www.luogu.com.cn/blog/trsins/post-xue-shu-color-coding-sui-ji-ran-se) 开个文章。 ```cpp #include<bits/stdc++.h> using namespace std; const int N=234; const int S=1<<6; const int inf=0x3f3f3f3f; int T,n,m,k,vis[N][N],dp[N][N][S],w[N][N],c[N][N],a[N][N],C[N],col[N],cid,fx[4]={1,-1,0,0},fy[4]={0,0,1,-1}; queue<pair<int,int>>q; inline bool check(int i,int j){return i>=1&&i<=n&&j>=1&&j<=m;} inline void spfa(int s){ while(!q.empty()){ auto t=q.front();q.pop(); int x=t.first,y=t.second;vis[x][y]=0; for(int i=0;i<4;i++){ int xx=x+fx[i],yy=y+fy[i]; if(!check(xx,yy)||c[xx][yy]==-1)continue; if(dp[xx][yy][s]>dp[x][y][s]+w[xx][yy]){ dp[xx][yy][s]=dp[x][y][s]+w[xx][yy]; if(!vis[xx][yy])q.push({xx,yy}),vis[xx][yy]=1; } } } return; } inline int Steiner(){ int shuffle=200,ans=inf; while(shuffle--){ random_shuffle(col+1,col+cid+1); for(int i=1;i<=cid;i++)C[col[i]]=i%k; for(int i=1;i<=n;i++)for(int j=1;j<=m;j++)for(int s=0;s<(1<<k);s++)dp[i][j][s]=inf; for(int i=1;i<=n;i++)for(int j=1;j<=m;j++)if(~c[i][j])dp[i][j][1<<C[c[i][j]]]=w[i][j]; for(int s=1;s<(1<<k);s++){ for(int i=1;i<=n;i++)for(int j=1;j<=m;j++)if(~c[i][j]){ for(int t=s&(s-1);t;t=(t-1)&s)dp[i][j][s]=min(dp[i][j][s],dp[i][j][t]+dp[i][j][s^t]-w[i][j]); if(dp[i][j][s]!=inf)q.push({i,j}); } spfa(s); } for(int i=1;i<=n;i++)for(int j=1;j<=m;j++)ans=min(ans,dp[i][j][(1<<k)-1]); } return ans; } inline void solve(){ scanf("%d%d%d",&n,&m,&k),cid=0; for(int i=1;i<=n;i++)for(int j=1;j<=m;j++)scanf("%d",&c[i][j]),col[++cid]=c[i][j]; for(int i=1;i<=n;i++)for(int j=1;j<=m;j++)scanf("%d",&a[i][j]),w[i][j]=1; sort(col+1,col+cid+1),cid=unique(col+1,col+cid+1)-col-1; int cnt=Steiner(); if(cnt==inf){puts("-1 -1");return;} int l=0,r=1e6,Mid=0; while(l<=r){ int mid=(l+r)>>1; for(int i=1;i<=n;i++)for(int j=1;j<=m;j++)w[i][j]=a[i][j]<=mid?9999:10001; if(Steiner()<=cnt*10000)r=mid-1,Mid=mid; else l=mid+1; } printf("%d %d\n",cnt,Mid); return; } int main(){ scanf("%d",&T); while(T--)solve(); return 0; } ``` - CF482D Random Function and Tree - 定义了一种染色 dfs,具体地,将当前点染上$ $访问过的点数的奇偶性,将儿子按照升序/降序随机其中一种排序后遍历,每个点有 $\frac 1 2$ 被访问,求不同染色方案。$n\le 10^5$。 一个显然的 dp 是抓住奇偶性的特点,$f_{u,0/1}$ 表示 $u$ 子树内染了的点的点数奇偶性为 $0/1$ 的方案数,转移容易。对于将儿子降序遍历相当于 $f$ 乘二即可,但是会有算重的情况。什么情况会在升、降序看来是一样的?对于每个点设 $pre_u,suf_i$ 表示从前往后和从后往前到该点时访问的点数的奇偶性,那么只有 $\forall v\in child_u,pre_u=suf_u$ 时才会算重,那么显然只可能是每个子树都染了偶数个点,或者是每个子树染了奇数个点且子树个数为奇数。对于这个子问题同样使用 dp:$g_{u,0/1,0/1}$ 表示当前子树是染点数的奇偶性,和之前所有子树都染点数为 $0/1$,转移也是容易的。注意实现中 $g$ **未算上自己即点 $u$**,所以 $f_{u,0}$ 减去的是 $g_{u,1,1}$,$f_{u,1}$ 同理。 ```cpp #include<bits/stdc++.h> using namespace std; #define int long long const int mod=1e9+7; const int N=1e5+6; int n,f[N][2],g[N][2][2]; vector<int>G[N]; void dfs(int u){ for(int v:G[u])dfs(v); f[u][1]=1,g[u][0][0]=g[u][0][1]=1; for(int v:G[u]){ int t0=(f[u][0]*f[v][0]%mod+f[u][1]*f[v][1]%mod)%mod; int t1=(f[u][1]*f[v][0]%mod+f[u][0]*f[v][1]%mod)%mod; f[u][0]=(f[u][0]+t0)%mod,f[u][1]=(f[u][1]+t1)%mod; int t00=g[u][0][0]*f[v][0]%mod; int t01=g[u][1][1]*f[v][1]%mod; int t11=g[u][0][1]*f[v][1]%mod; g[u][0][0]=(g[u][0][0]+t00)%mod,g[u][0][1]=(g[u][0][1]+t01)%mod,g[u][1][1]=(g[u][1][1]+t11)%mod; } f[u][0]=(2*f[u][0]%mod-g[u][1][1]+mod)%mod,f[u][1]=(f[u][1]*2%mod-g[u][0][0]+mod)%mod; return; } signed main(){ scanf("%lld",&n); for(int i=2;i<=n;i++){ int v=i,u;scanf("%lld",&u); G[u].push_back(v); } dfs(1); printf("%lld\n",(f[1][0]+f[1][1])%mod); return 0; } ``` - CF1870F Lazy Numbers - 将 $k$ 进制下的正整数 $1∼n$ 按字典序排序得到 $p_1,p_2,…,p_n,$ 求满足 $p_x=x$ 的 $x$ 的数量。$T\le 1000,n,k\le 10^{18}$。 差点不会做呜呜呜。 我们考虑对于所有长度为 $l+1$ 的数 $x\in[k^l,k^{l+1})$,长度相同的数,其字典序排名也随值上升而增加。所以对于任意两个长度都为 $l$ 的数 $x,y(x<y)$,那么显然有 $p_y-p_x>0$,而题目所求相当于 $\sum _x[p_i-i=0]$,所以定义 $p'_i=p_i-i$,上式转为 $p'_y-p'_x\ge 0$,所以将 $p'$ 按照长度分为 $\log_kn$ 段函数,每一段都是**单调不降**的,所以每段函数内 $p'=0$ 的一定为一段区间。 这启发我们去枚举长度 $l$,然后二分区间的左右端点,复杂度为 $O(\log^2\text{Solve}(x))$,其中 $\text{Solve}(x)$ 表示求出 $p_x$ 的值的复杂度。 处理这个,我们自然地想到对于长度分类: - 长度为 $l$ 的数 $y$ 字典序小于等于 $x$ 的有 $x-k^{l}+1$ 个。 - 长度为 $i<l$ 的数 $y$ 字典序小于等于 $x$ 的有 $\left\lfloor \dfrac{x}{k^{l-i}} \right\rfloor - k^i + 1$ 个。 - 长度为 $j>l$ 的数 $y$ 字典序小于等于 $x$ 的有 $xk^{j-l} - k^j$ 个。 总时间复杂度 $O(T\log_k^3n)$。注意 $n\times k$ 会溢出 `long long`。 ```cpp #include<bits/stdc++.h> using namespace std; #define int long long int n,k,pw[80],ans,m,T; inline int calc(int x,int p){ int res=x-pw[p]+1,t=x; for(int i=p-1;i>=0;i--)t/=k,res+=t-pw[i]+1; t=x;for(int i=p+1;i<=m;i++){ if(n/k>=t&&t*k<=n)t=min(t*k,n+1); else t=n+1; res+=t-pw[i]; } return res; } void solve(){ scanf("%lld%lld",&n,&k); pw[m=0]=1,ans=0; while(n/k>=pw[m]&&pw[m]*k<=n)pw[m+1]=pw[m]*k,m++; for(int i=0;i<=m;i++){ int l=pw[i],r=(i<m?pw[i+1]-1:n); int l1=l,r1=r,L=r+1; while(l1<=r1){ int mid=(l1+r1)>>1; if(calc(mid,i)>=mid)L=mid,r1=mid-1; else l1=mid+1; } int l2=l,r2=r,R=l-1; while(l2<=r2){ int mid=(l2+r2)>>1; if(calc(mid,i)<=mid)R=mid,l2=mid+1; else r2=mid-1; } if(L<=R)ans+=R-L+1; } printf("%lld\n",ans); } signed main(){ scanf("%lld",&T); while(T--)solve(); return 0; } ``` - Gym103687E Easy Jump - $n$ 个关卡,有些关卡可以回蓝,你初始有生命值 $H$ 和蓝量 $S$,给定每个关卡通过的概率 $p_i$,在每个关卡,你可以: 1. 花费 $1$ 单位时间尝试通关,有 $p_x$ 的概率通过,进入下一关,否则扣除一单位血量并留在当前关。 2. 花费 $T_1$ 单位时间,消耗一单位蓝量,恢复一单位血量, 3. 花费 $T_2$ 单位时间,不消耗蓝量,恢复一单位血量, 4. 如果关卡 $x$ 是可以回蓝的关卡,则可以恢复任意单位蓝量,不花费时间。 求按照最优策略保证角色不死的情况下所需的最小期望通关时间。$n\le 1000,2\le H\le9,S\le 6$。 $H,S$ 开这么小只是为了契合原题面 Hollow Knight 中的取值:)。 先考虑 $S=0$ 或 $T1\ge T2$ 的情况,此时显然不会选择回蓝。 一个显然的贪心策略是一直尝试通关知道血量为 $1$ 才会选择回血。设 $f_{i,j}$ 表示在第 $i$ 关,血量为 $j$ 的通关概率,转移写出来后从后往前倒退即可。 对于 $S>0$ 且 $T1<T2$ 时会有回蓝操作,如果在不能回蓝的关卡那么转移与之前差不多。考虑在可以回蓝的关卡时,首先我们肯定是直接回满蓝,然后在闯关前先回一定的血量,这个可以直接枚举,因为该关卡任何状态的蓝量都是满的,所以是 $O(H)$ 的。 ```cpp #include<bits/stdc++.h> using namespace std; const int N=1e3+4; const double inf=1e9; int n,H,S,T1,T2,m,fl[N]; double p[N],dp[N][7][10]; int main(){ scanf("%d%d%d",&n,&H,&S); for(int i=1;i<=n;i++)scanf("%lf",&p[i]),p[i]/=100; scanf("%d",&m); for(int i=1,x;i<=m;i++)scanf("%d",&x),fl[x]=1; scanf("%d%d",&T1,&T2); for(int i=1;i<=n;i++)for(int j=0;j<=S;j++)for(int k=1;k<=H;k++)dp[i][j][k]=inf; for(int i=n;i;i--){ int t=T2; if(S&&fl[i])t=min(T1,t); for(int j=0;j<=S;j++)for(int k=2;k<=H;k++){ dp[i][j][k]=min({ p[i]*dp[i+1][j][k]+(1-p[i])*dp[i][j][k-1]+1, (p[i]*dp[i+1][j][k]+(1-p[i])*t+1)/p[i], j?p[i]*dp[i+1][j][k]+(1-p[i])*(dp[i][j-1][k]+T1)+1:inf }); } if(fl[i]){ for(int j=S-1;j>=0;j--)for(int k=1;k<=H;k++)dp[i][j][k]=min(dp[i][j][k],dp[i][j+1][k]); if(S)for(int j=0;j<=S;j++)for(int k=H-1;k;k--)dp[i][j][k]=min(dp[i][j][k],dp[i][j][k+1]+T1); } } printf("%.9lf\n",dp[1][S][H]); return 0; } ``` - P4787 [BalkanOI2018] Minmaxtree - 有一棵有 $N$ 个结点的无权树,结点分别编号为 $1\dots N$。现在要给每条边赋一个权值,使之变为一棵带权树。这棵带权树满足 $K$ 个条件,条件分为两类: 1. $\ \texttt{M }x\ y\ z\ \ $ 从结点 $x$ 到结点 $y$ 的链上最大的边权为 $z$; 2. $\ \texttt{m }x\ y\ z\ \ $ 从结点 $x$ 到结点 $y$ 的链上最小的边权为 $z$。 保证这 $K$ 组条件的 $z$ 互不相同,构造出这棵树,并输出每条边的边权。$n,m\le 7\times 10^4$。 假设我们已知每条边的取值范围 $[L_i,R_i]$,不难证明必定存在一种方案使得每条边选择的是 $L_i$ 或者 $R_i$(由于题目保证 $L_i,R_i$ 等互不相同)。所以问题就转化成了给每种权值找一条边使得所有权值都出现,直接使用网络流进行二分图匹配 $O(n\sqrt n)$。 对于求出权值范围可以使用树剖,但是一种码量小的做法是:将权值排序后依次加入,这样每次操作就变成了给路径上还没有染色的边染上一种颜色,直接并查集维护每个点第一个没染色的祖先即可,复杂度 $O(n\log n+n\alpha (n))$。 ```cpp #include<bits/stdc++.h> using namespace std; const int N=7e4+5; const int inf=1e9; int n,m,fa[N],dis[N<<1],dep[N],top[N],sz[N],son[N],Fa[N],lsh[N],head[N<<1],cur[N<<1],tot=1,S,T; vector<int>g[N]; struct node{ int x,y,v; node(int X=0,int Y=0,int V=0){x=X,y=Y,v=V;} }; vector<node>mx,mn; struct edge{int to,nxt,w;}e[N<<4]; void addedge(int u,int v,int w){ e[++tot].to=v; e[tot].nxt=head[u]; head[u]=tot; e[tot].w=w; return; } void add(int u,int v){ addedge(u,v,1),addedge(v,u,0); return; } void dfs1(int u,int faa){ dep[u]=dep[fa[u]=faa]+1,sz[u]=1; for(int v:g[u])if(v!=faa){ dfs1(v,u); sz[u]+=sz[v]; if(sz[son[u]]<sz[v])son[u]=v; } return; } void dfs2(int u,int tp){ top[u]=tp; if(son[u])dfs2(son[u],tp); for(int v:g[u]){ if(v==fa[u]||v==son[u])continue; dfs2(v,v); } return; } int lca(int u,int v){ while(top[u]!=top[v]){ if(dep[top[u]]<dep[top[v]])swap(u,v); u=fa[top[u]]; } return dep[u]<dep[v]?u:v; } bool bfs(){ queue<int>q; memset(dis,0,sizeof(dis)),q.push(S),dis[S]=1; while(!q.empty()){ int u=q.front();q.pop(); for(int i=head[u];i;i=e[i].nxt){ int v=e[i].to;cur[u]=head[u]; if(!dis[v]&&e[i].w)dis[v]=dis[u]+1,q.push(v); } } return dis[T]; } int dfs(int u,int flow){ if(u==T)return flow; int res=0; for(int&i=cur[u];i;i=e[i].nxt){ if(!flow)break; int v=e[i].to; if(e[i].w>0&&dis[v]==dis[u]+1){ int d=dfs(v,min(e[i].w,flow)); e[i].w-=d,e[i^1].w+=d,flow-=d,res+=d; } } return res; } int find(int x){return Fa[x]!=x?Fa[x]=find(Fa[x]):x;} int main(){ scanf("%d",&n); for(int i=1;i<n;i++){ int u,v; scanf("%d%d",&u,&v); g[u].push_back(v),g[v].push_back(u); } dfs1(1,0),dfs2(1,1); scanf("%d",&m); for(int i=1;i<=m;i++){ char ch;cin>>ch; int x,y,v;scanf("%d%d%d",&x,&y,&v),lsh[i]=v; if(ch=='M')mx.push_back(node(x,y,v)); else mn.push_back(node(x,y,v)); }sort(lsh+1,lsh+m+1); S=n+m+1;T=S+1; for(int i=1;i<=n;i++)add(S,i); for(int i=1;i<=m;i++)add(i+n,T); sort(mx.begin(),mx.end(),[&](node a,node b){return a.v<b.v;}); for(int i=1;i<=n;i++)Fa[i]=i; for(auto t:mx){ int u=t.x,v=t.y,o=lca(u,v),pos=lower_bound(lsh+1,lsh+m+1,t.v)-lsh,p; while(dep[p=find(u)]>dep[o])add(p,pos+n),Fa[p]=fa[p]; while(dep[p=find(v)]>dep[o])add(p,pos+n),Fa[p]=fa[p]; } sort(mn.begin(),mn.end(),[&](node a,node b){return a.v>b.v;}); for(int i=1;i<=n;i++)Fa[i]=i; for(auto t:mn){ int u=t.x,v=t.y,o=lca(u,v),pos=lower_bound(lsh+1,lsh+m+1,t.v)-lsh,p; while(dep[p=find(u)]>dep[o])add(p,pos+n),Fa[p]=fa[p]; while(dep[p=find(v)]>dep[o])add(p,pos+n),Fa[p]=fa[p]; } while(bfs())dfs(S,inf); for(int u=2;u<=n;u++){ int res=0; for(int i=head[u];i;i=e[i].nxt){ int v=e[i].to; if(!e[i].w&&v<=n+m)res=v-n; } if(!res)for(int i=head[u];i;i=e[i].nxt){ int v=e[i].to; if(v<=n+m)res=v-n; } printf("%d %d %d\n",fa[u],u,lsh[res]); } return 0; } ``` - CF1864H Asterism Stream - 给定一变量,初始为 $1$,每次等概率随机进行以下两种操作之一,令 $x$ 加一或者令 $x$ 乘二。求期望多少次操作之后 $x$ 会 $\ge n$。$T$ 组数据,$T\le 100,n\le 10^{18}$。 官方题解 $O(\log^2n)$ 很好懂自己看。写的是 amiya 在讨论区提出的一种思路很自然的 $O((T+\log n)\log^3n)$ 的做法。 设 $f_i$ 表示 $i$ 被遍历的期望次数,$f$ 的转移式容易写出,但是 $O(n)$ 的做法不够优秀,注意到前继状态很少,考虑能否使用线性递推或者矩阵表示。我们发现对于 $f_i$,我们只关注 $ctz(i)$,使用向量 $A_i$ 来统计所有 $f_{i/2^k}$。 定义矩阵 $F$ 使 $f_i=F_{ctz(i)}\times f_{i-1}$。那么可求出 $A_i=A_{i-1}\times F_{ctz(i)}$。为了使其能够类似矩阵快速幂,再定义 $G_i=\prod_{j=1}^{2^{i+1}-1}F_j,H_i=\prod_{j=1}^{2^i}F_j$,那么 $G_0=H_0=F_0$,$H_i\times G_{i-1}=F_i$,$G_i=H_i\times G_{i-1}$。 ```cpp #include<bits/stdc++.h> using namespace std; #define int long long const int N=60; const int mod=998244353; const int inv2=(mod+1)/2; int T,n; struct matrix{ int t[N+1][N+1]; matrix(){memset(t,0,sizeof(t));} matrix operator*(const matrix&rhs)const{ matrix res; for(int i=0;i<=N;i++) for(int k=0;k<=i;k++) for(int j=0;j<=k;j++) res.t[i][j]=(res.t[i][j]+t[i][k]*rhs.t[k][j]%mod)%mod; return res; } matrix operator/(const matrix&rhs)const{ matrix res; for(int k=0;k<=N;k++) for(int j=0;j<=k;j++) res.t[0][j]=(res.t[0][j]+t[0][k]*rhs.t[k][j]%mod)%mod; return res; } }f[N],g[N],h[N],A; void SolveF(){ for(int i=0;i<N;i++){ f[i].t[0][0]=f[i].t[1][0]=1; for(int j=0;j<=i;j++){ int coef=inv2; for(int k=j;k<i;k++){ f[i].t[k+1][j+1]=coef; coef=coef*inv2%mod; } f[i].t[i+1][j+1]=coef; } for(int j=i+1;j<N;j++)f[i].t[j+1][j+1]=1; } return; } void SolveG(){ g[0]=h[0]=f[0]; for(int i=1;i<N;i++){ h[i]=g[i-1]*f[i]; g[i]=h[i]*g[i-1]; } return; } void init(){ for(int i=1;i<=N;i++)A.t[0][i]=2; return; } void solve(){ scanf("%lld",&n); matrix ans=A; for(int i=59;i>=0;i--)if(n&(1ll<<i))ans=ans/h[i]; printf("%lld\n",(ans.t[0][0]-2+mod)%mod); return; } signed main(){ SolveF(),SolveG(),init(); scanf("%lld",&T); while(T--)solve(); return 0; } ``` - CF1753F Minecraft Series - 给定一个 $n\times m$ 的矩形,有 $k$ 个人有权值位于其中的坐标上(可能坐标重复),求有多少个选择正方形的方法使选择的正方形中可以选出一个长度 $\ge t$ 的权值连续序列 $l,l+1,\dots r-1,r$ 且 $l\le 0\le r$。$n,m\le 4\times 10^5,nm\le 4\times 10^5,k\le 10^6$。 怎么又差点不会。 枚举正方形转为枚举对角线,然后枚举正方形的左上角并双指针维护右下角,而每个人在对于一条对角线的情况中只会被加入一次、删除一次,所以至多被加入 $\min(n,m)\le \sqrt{nm}$。而对于询问则相当于对于正数和负数都做一个 mex 查询。 修改便是 $O(k\sqrt{nm})$ 级别,查询 $O(nm)$ 级别,需要平衡二者复杂度,使用值域分块将值域分为 $\sqrt k$ 段,修改 $O(1)$ 更改对应位的值以及对应段内的信息,查询扫过所有块来找到最小的有缺失元素的段,再在段内暴力查询,$O(\sqrt k)$。总时间复杂度 $O(k\sqrt{nm}+nm\sqrt k)$。注意实现卡常,将每个点内权值排序来做到连续内存访问。 ```cpp #pragma GCC optimize("Ofast") #include<bits/stdc++.h> using namespace std; const int N=4e4+5; const int M=1e6+7; const int inf=1e9; const int S=1e3+4; int n,m,k,lim,cnt[2][M],vis[2][S],anger[M],L,R,block,ans; vector<vector<int>>a[N]; inline int read(){ int x=0,fl=1;char ch;ch=getchar(); while(ch<'0'||ch>'9'){if(ch=='-')fl=-1;ch=getchar();} while(ch>='0'&&ch<='9')x=(x<<1)+(x<<3)+ch-'0',ch=getchar(); return x*fl; } inline void write(int x){ int tmp[11],cnt=0; if(!x)putchar('0'); while(x)tmp[++cnt]=x%10,x/=10; while(cnt)putchar(tmp[cnt--]+'0'); putchar('\n'); } inline void add(int v){ if(v<L||v>R)return; int o=v>=0;v=abs(v); if(!cnt[o][v])vis[o][v/block]++; cnt[o][v]++;return; } inline void del(int v){ if(v<L||v>R)return; int o=v>=0;v=abs(v); if(cnt[o][v]==1)vis[o][v/block]--; cnt[o][v]--;return; } inline void Add(int x1,int y1,int x2,int y2){ for(int i=x1;i<=x2;i++)for(int v:a[i][y2])add(v); for(int j=y1;j<y2;j++)for(int v:a[x2][j])add(v); return; } inline void Del(int x1,int y1,int x2,int y2){ for(int i=x1;i<=x2;i++)for(int v:a[i][y1])del(v); for(int j=y1+1;j<=y2;j++)for(int v:a[x1][j])del(v); return; } inline int qry(){ int res=0; for(int o=0;o<2;o++){ int k=0; while(vis[o][k]==block)k++; for(int i=k*block;;i++)if(!cnt[o][i]){ res+=i; break; } } return res-1; } int main(){ n=read(),m=read(),k=read(),lim=read(),block=ceil(sqrt(k)); for(int i=1;i<=n;i++)a[i].resize(m+1); for(int i=1;i<=k;i++){ int x=read(),y=read(),v=read(); a[x][y].push_back(v),anger[i]=v; }anger[++k]=0; for(int i=1;i<=n;i++)for(int j=1;j<=m;j++)if(a[i][j].size())sort(a[i][j].begin(),a[i][j].end()); sort(anger+1,anger+k+1),anger[0]=-inf,anger[k+1]=inf; int pos=lower_bound(anger+1,anger+k+1,0)-anger; L=anger[pos];for(int i=pos;anger[i]-anger[i-1]<2;i--)L=anger[i-1]; R=anger[pos];for(int i=pos;anger[i+1]-anger[i]<2;i++)R=anger[i+1]; cnt[0][0]=cnt[1][0]=vis[0][0]=vis[1][0]=1; for(int j=1;j<=m;j++){ int x1=1,y1=j,x2=1,y2=j; while(x2<=n&&y2<=m){ Add(x1,y1,x2,y2); while(qry()>=lim&&x1<=x2)Del(x1,y1,x2,y2),x1++,y1++; x2++,y2++,ans+=x1-1; }x2--,y2--; while(x1<=x2)Del(x1,y1,x2,y2),x1++,y1++; } for(int i=2;i<=n;i++){ int x1=i,y1=1,x2=i,y2=1; while(x2<=n&&y2<=m){ Add(x1,y1,x2,y2); while(qry()>=lim&&x1<=x2)Del(x1,y1,x2,y2),x1++,y1++; x2++,y2++,ans+=y1-1; }x2--,y2--; while(x1<=x2)Del(x1,y1,x2,y2),x1++,y1++; }write(ans); return 0; } ``` - [AGC029E] Wandering TKHS - 给定一棵 $n$个点的树,你现在在某个点 $s$。初始时你有集合 $S$ 且只有 $s$。每次你会选择所以未达到的且与集合中的点有直接连边的点中编号最小的点,并加入集合。当集合中加入 $1$ 立即结束。对于一个 $s$ 定义最终集合大小为 $C_s$,求 $\forall s\in(1,n],C_s$。$n\le 2\times 10^5$。 太能诈骗了。定义 $rt=1$。自然地拆贡献,我们考虑一点 $u$ 能否被计算进 $C_s$ 里面。令 $w=lca(u,s)$,则 $s$ 必定是先走到 $w$ 才会考虑是否走去 $u$。那么接下来的过程便与 $s$ 无关。 计算什么样的 $v$ 不能被计算进 $w$,容易发现当 $w\to rt$ 的最大值(不包括 $w$)小于 $u\to w$ 最大值(不包括 $w$)时显然不会走到 $u$。 你发现这对于 $v$ 的限制很松,再拆贡献,转化为怎么样的 $w$ 能使得 $u$ 作出贡献。那么设 $mx$ 表示 $u\to rt$ 上最大值的点,$mx$ 下方的 $w$ 显然必定经过 $u$,$w$ 上方的必定不经过 $u$。而对于 $w=mx$ 的情况再记录一个 $se$ 表示$u\to rt$ 上最次值的点,判断 $mx,se$ 的深度关系即可。那么合法的 $w$ 关于 $u$ 的合法点集便是一条路径,那么对应回去的 $s$ 便是一个子树。树上差分。$O(n)$。 ```cpp #include<bits/stdc++.h> using namespace std; const int N=2e5+6; int n,tot,head[N],t[N],mx[N],se[N],son[N],dep[N]; struct edge{int to,nxt,w;}e[N<<1]; void addedge(int u,int v){ e[++tot].to=v; e[tot].nxt=head[u]; head[u]=tot; return; } void dfs(int u,int fa){ dep[u]=dep[fa]+1; if(u>mx[u])se[u]=mx[u],mx[u]=u; else se[u]=max(se[u],u); if(u^1)t[dep[se[u]]<dep[mx[u]]?mx[u]:son[mx[u]]]++; for(int i=head[u];i;i=e[i].nxt){ int v=e[i].to; if(v==fa)continue; son[u]=v,mx[v]=mx[u],se[v]=se[u],dfs(v,u); } return; } void Dfs(int u,int fa){ t[u]+=t[fa]; for(int i=head[u];i;i=e[i].nxt){ int v=e[i].to; if(v==fa)continue; Dfs(v,u); } return; } int main(){ scanf("%d",&n); for(int i=1;i<n;i++){ int u,v; scanf("%d%d",&u,&v); addedge(u,v),addedge(v,u); } dfs(1,0),Dfs(1,0); for(int i=2;i<=n;i++)printf("%d ",t[i]); return 0; } ``` - [AGC016E] Poor Turkeys - 共有 $n$ 只火鸡,并给出了 $m$ 对关系,形如两只火鸡,将按照给出关系的顺序,对于每一对火鸡随机选择一个吃掉。被吃掉的火鸡就死了。两只都被吃了就不会操作。说白了就是一只火鸡只能被吃一次。求有多少对 $(i,j)$ 表示 $i,j$ 这两只火鸡能活下来。$n\le 400$。 先时间倒流。考虑钦定活下来一只的情况,设它是 $x$,称它为被保护的。那么对于 $x$ 所有所在的关系,另一只火鸡必须保证在它与 $x$ 所在关系之前不能被吃。那么相当于这些火鸡也是被保护的。 所以对于一只最终存活的火鸡,会形成一个集合 $S$ 保证其中的火鸡都是被保护的。什么情况下会不合法?当有一组关系中的两只火鸡都在集合中就寄了。 两只鸡同理。一只鸡只能死一次。所以看 $S_i\cap S_j$ 是否有交即可。你可以用 bitset 优化,反正数据范围小。 ```cpp #include<bits/stdc++.h> using namespace std; const int N=420; const int M=1e5+6; int n,m,x[M],y[M],ans,fl[N]; bitset<N>S[N]; int main(){ scanf("%d%d",&n,&m); for(int i=1;i<=m;i++)scanf("%d%d",&x[i],&y[i]); for(int i=1;i<=n;i++){ S[i].set(i); for(int j=m;j;j--){ int u=S[i][x[j]],v=S[i][y[j]]; if(u&&v){fl[i]=1;break;} if(u)S[i].set(y[j]); if(v)S[i].set(x[j]); } } for(int i=1;i<n;i++)if(!fl[i]){ for(int j=i+1;j<=n;j++)if(!fl[j]){ bool fl=1; for(int k=1;k<=n;k++)if(S[i][k]&&S[j][k]){fl=0;break;} ans+=fl; } } printf("%d",ans); return 0; } ``` - CF1767E Algebra Flash - 有一个长度为 $n$ 的序列,你需要从 $1$ 开始走到 $n$。每一次可以向前 $1$ 或 $2$ 步。每个格子有个颜色 $a_i$,共 $m$ 种,每一种颜色有花费 $x_i$,你需要买下你经过的所有颜色。求最小花费。$1 \leq n \leq 3 \times 10 ^ 5, 1 \leq m \leq 40$。 $m\le 40$ 显然类似与折半然后状压 dp。 将题目用限制写出来:$a_1,a_n$ 必选,任意两个相邻点的颜色必选。 那么这很像**最小点覆盖**,将 $a_1\to a_1,a_n\to a_n,\forall i,a_i\to a_{i+1}$。那么这确实就是最小点覆盖。最小点覆盖=权值和-最大权独立集。所以转为求最大独立集。使用状压 dp 然后用 map 记忆化搜索即可。具体即设 $dp_S$ 表示集合 $S$ 中的最大独立集权值和。 所以复杂度为什么是对的呢?我们考虑优化 dp 转移,每次考虑删除当前编号最小的点。那么,当 $lowbit(S)<m/2$ 时,从全集转移到 $S$ 的过程必定是只减去了 $<lowbit(S)$ 的位置,那么状态只有 $<2^{m/2}$ 种。否则意味着 $[0,m/2)$ 都被选择,对于 $[m/2,m]$ 也同样有 $2^{m/2}$ 种状态。所以复杂度是对的。$O(n+m2^{m/2})$。 ```cpp #include<bits/stdc++.h> using namespace std; #define int long long const int N=42; const int M=3e5+6; int n,m,col[M],val[N],g[N],ans; map<int,int>dp; void add(int u,int v){ g[u]|=1ll<<v; g[v]|=1ll<<u; } int dfs(int s){ if(dp.count(s))return dp[s]; int x=log2(s&-s); int res=dfs(s^(1ll<<x)); if(!(g[x]&(1ll<<x)))res=max(res,dfs(s^(1ll<<x)^(s&(g[x])))+val[x]); return dp[s]=res; } signed main(){ scanf("%lld%lld",&n,&m); for(int i=1;i<=n;i++)scanf("%lld",&col[i]),col[i]--; for(int i=2;i<=n;i++)add(col[i-1],col[i]); add(col[1],col[1]),add(col[n],col[n]); for(int i=0;i<m;i++)scanf("%lld",&val[i]),ans+=val[i]; dp[0]=0,ans-=dfs((1ll<<m)-1); printf("%lld",ans); return 0; } ``` - loj6062. 「2017 山东一轮集训 Day2」Pair - 给出一个长度为 n 的数列 $a_i $ 和一个长度为 m 的数列 $ b_i$,求 $a_i$ 有多少个长度为 $m$ 的连续子序列能与 $b_i$ 匹配。两个数列可以匹配,当且仅当存在一种方案,使两个数列中的数可以两两配对,两个数可以配对当且仅当它们的和不小于 $h$。$n,m\le 1.5\times 10^5$。 先将 $b$ 排序,将 $a_i$ 看作是 $h-a_i$,那么配对相当于 $a_i\le b_i$,故每个 $a_i$ 可以对应的 $b_i$ 是一段后缀。根据霍尔定理得一段连续子序列合法当且仅当对于每个后缀有 $suf_i\ge i$,其中 $suf_i$ 表示 $a_i$ 中 $i$ 开始的这段后缀能够对应的 $b$ 后缀之和。 所以使用线段树维护,初始设 $b_i=-i$,每次加入一个 $a_i$ 就给 $[a_i,m]$ 加一,一段区间是否合法便查询全局最大值是否 $\ge 0$。$O(n\log n)$。 ```cpp #include<bits/stdc++.h> using namespace std; #define ls p<<1 #define rs p<<1|1 const int N=1.5e5+6; int n,m,h,tr[N<<2],tag[N<<2],a[N],b[N],ans; void pushup(int p){ tr[p]=min(tr[ls],tr[rs]); return; } void pushdown(int p){ if(!tag[p])return; tr[ls]+=tag[p],tr[rs]+=tag[p],tag[ls]+=tag[p],tag[rs]+=tag[p]; tag[p]=0;return; } void build(int p,int l,int r){ if(l==r){tr[p]=-l;return;} int mid=(l+r)>>1; build(ls,l,mid),build(rs,mid+1,r); pushup(p); return; } void update(int p,int l,int r,int s,int t,int val){ if(s>t)return; if(s<=l&&r<=t){ tr[p]+=val,tag[p]+=val; return; } int mid=(l+r)>>1; pushdown(p); if(s<=mid)update(ls,l,mid,s,t,val); if(t>mid)update(rs,mid+1,r,s,t,val); pushup(p); return; } int main(){ scanf("%d%d%d",&n,&m,&h); for(int i=1;i<=m;i++)scanf("%d",&b[i]); sort(b+1,b+m+1),build(1,1,m); for(int i=1;i<=n;i++)scanf("%d",&a[i]),a[i]=lower_bound(b+1,b+m+1,h-a[i])-b; for(int i=1;i<m;i++)update(1,1,m,a[i],m,1); for(int i=m;i<=n;i++){ if(i>m)update(1,1,m,a[i-m],m,-1); update(1,1,m,a[i],m,1),ans+=(tr[1]>=0); } printf("%d",ans); return 0; } ``` - CF1879F Last Man Standing - 给定 $n$ 个人,每个人有两个属性 $h_i,a_i$。定义一个难度为 $x$ 的轮为:每回合对于所有人将当前的 $a$ 减去 $x$,对于 $a$ 被减到 $\le 0$ 的所有人,其 $a$ 将恢复到 $a_i$,且 $h_i$ 减一。当一个人的 $h_i$ 为 $0$ 时推出该轮。此轮游戏将一直回合下去直到剩下仅剩一人,则此人获得积分,积分为该选手作为仅剩的一人的轮数。如最后局面剩下多个人并在同一回合中一起退出,则所有人都无积分。现在 $\forall x> 0$ 都会运行一次难度为 $x$ 的轮,轮与轮之间相互独立,求每个人可以获得最多的积分是多少。$n,h_i,a_i\le 2\times 10^5$。 还是建议看英文原题面吧。 对于一个 $x$,显然每个选手会在第 $k=\left\lceil\dfrac{a}{x}\right\rceil \times h$ 回合退出。而最终存活下来的选手即 $k$ 最大的选手,并获得 $k_{mx}-k_{se}$ 的积分,即存货最久的人的轮数减去第二久的人的轮数。 注意到 $h,a$ 值域很小,可以枚举 $x$,然后对于每种 $x$ 都计算出当前的答案。$\left\lceil\dfrac{a}{x}\right\rceil$ 启发我们对于 $c=\left\lceil\dfrac{a}{x}\right\rceil$ 的取值进行分类,那么对于 $a_i\in[(c-1)x+1,cx]$ 的所有人我们只关心其中 $h$ 的最大值与次大值,因为我们只关系全局上的最大值和次大值。 如果我们知道每段区间的 $h_{mx},h_{se}$,那么我们可以暴力便利每个 $c$ 的区间然后得出最大与次大值,在 $O(A\log A)$ 的时间复杂度内完成。 若只查询最大值便是模板的 ST 表,对于次大值的维护同样可以使用 ST 表,我们记录最大值出现位置的极左和极右从而判断当前的 $se$ 能否取到 $mx$。更具体见 [ST表维护区间次大值](https://rockyyue.blog.luogu.org/st-biao-wei-hu-ou-jian-ci-tai-zhi)。总复杂度 $O(n+A\log A)$。 ```cpp #include<bits/stdc++.h> using namespace std; #define ll long long #define mem(x) memset(x,0,sizeof(x)) const int N=2e5+6; const int K=20; int T,h[N],a[N],n,L[K][N],R[K][N],mx[K][N],se[K][N],edpos[N],lg[N]; ll ans[N],edmx[N],edse[N]; int qrymx(int l,int r){ int k=lg[r-l+1]; return max(mx[k][l],mx[k][r-(1<<k)+1]); } int qryse(int l,int r){ int k=lg[r-l+1]; if(mx[k][l]==mx[k][r-(1<<k)+1]&&L[k][l]!=R[k][r-(1<<k)+1])return mx[k][l]; else if(mx[k][l]>mx[k][r-(1<<k)+1])return max(se[k][l],mx[k][r-(1<<k)+1]); else if(mx[k][r-(1<<k)+1]>mx[k][l])return max(se[k][r-(1<<k)+1],mx[k][l]); else return max(se[k][l],se[k][r-(1<<k)+1]); } int qrypos(int l,int r){ int k=lg[r-l+1]; if(mx[k][l]>=mx[k][r-(1<<k)+1])return L[k][l]; else return R[k][r-(1<<k)+1]; } void solve(){ scanf("%d",&n);int A=0; mem(ans),mem(L),mem(R),mem(mx),mem(se),mem(edpos),mem(edmx),mem(edse); for(int i=1;i<=n;i++)scanf("%d",&h[i]); for(int i=1;i<=n;i++)scanf("%d",&a[i]),A=max(A,a[i]); for(int i=1;i<=n;i++){ if(mx[0][a[i]]<h[i]){ se[0][a[i]]=mx[0][a[i]],mx[0][a[i]]=h[i]; L[0][a[i]]=R[0][a[i]]=i; } else if(se[0][a[i]]<h[i])se[0][a[i]]=h[i]; } for(int k=1;k<=lg[A]+1;k++)for(int i=1;i+(1<<k)-1<=A;i++){ int j=i+(1<<k-1),lmx=mx[k-1][i],rmx=mx[k-1][j]; mx[k][i]=max(lmx,rmx); if(lmx==rmx)L[k][i]=L[k-1][i],R[k][i]=R[k-1][j]; else if(lmx>rmx)L[k][i]=L[k-1][i],R[k][i]=R[k-1][i]; else L[k][i]=L[k-1][j],R[k][i]=R[k-1][j]; se[k][i]=max(se[k-1][i],se[k-1][j]); if(lmx!=rmx)se[k][i]=max(se[k][i],min(lmx,rmx)); if(lmx==rmx&&L[k][i]!=R[k][i])se[k][i]=mx[k][i]; } for(int x=1;x<=A;x++){ for(int l=1;l<=A;l+=x){ int r=min(A,l+x-1),c=(l-1)/x+1,pos=qrypos(l,r); ll Mx=1ll*qrymx(l,r)*c,Se=1ll*qryse(l,r)*c; if(Mx>edmx[x])edse[x]=edmx[x],edmx[x]=Mx,edpos[x]=pos; else if(Mx>edse[x])edse[x]=Mx; edse[x]=max(edse[x],Se); } ans[edpos[x]]=max(ans[edpos[x]],edmx[x]-edse[x]); } for(int i=1;i<=n;i++)printf("%lld ",ans[i]);puts(""); return; } int main(){ scanf("%d",&T); for(int i=2;i<N;i++)lg[i]=lg[i>>1]+1; while(T--)solve(); return 0; } ``` - CF1221F Choose a Square - 在平面直角坐标系上给定 $n$ 个带权值的点,求权值最大的正方形且左下角与右上角都在 $y=x$ 直线上。定义一个正方形的权值为正方形边上及内部所有点权值之和减去正方形边长。$n\le 5\times 10^5$。 好蠢。考虑一个点怎么样会被包含进一个正方形内,或者说,哪些正方形会包含它?显然,设正方形左下角是 $(l,l)$,右上角 $(r,r)$,那么所有 $l\le x,y\le r$ 的点 $(x,y)$ 都在其中。 即 $\min(x,y)\ge l,\max(x,y)\le r$,那么每个点就转为一段区间 $[\min(x,y),\max(x,y)]$,问题便是求一段区间使得该区间覆盖线段权值和最大。至于边长 $r-l$ 初始时在线段树上赋值为 $-l$ 即可。$O(n\log n)$。 ```cpp #include<bits/stdc++.h> using namespace std; #define ls p<<1 #define rs p<<1|1 #define int long long const int N=1e6+7; const int inf=0x3f3f3f3f3f3f3f3f; int n,m,tr[N<<2],tag[N<<2],pos[N<<2],tot,lsh[N],L,R,ans=-inf; vector<pair<int,int>>q[N]; struct node{int l,r,w;}a[N]; void pushup(int p){ tr[p]=max(tr[ls],tr[rs]); if(tr[p]==tr[ls])pos[p]=pos[ls]; else pos[p]=pos[rs]; return; } void pushdown(int p){ if(!tag[p])return; tag[ls]+=tag[p],tag[rs]+=tag[p],tr[ls]+=tag[p],tr[rs]+=tag[p]; tag[p]=0;return; } void build(int p,int l,int r){ if(l==r){ tr[p]=-lsh[l],pos[p]=l; return; } int mid=(l+r)>>1; build(ls,l,mid),build(rs,mid+1,r); pushup(p); return; } void update(int p,int l,int r,int s,int t,int val){ if(s<=l&&r<=t){ tr[p]+=val,tag[p]+=val; return; } int mid=(l+r)>>1; pushdown(p); if(s<=mid)update(ls,l,mid,s,t,val); if(t>mid)update(rs,mid+1,r,s,t,val); pushup(p); return; } auto query(int p,int l,int r,int s,int t){ if(s<=l&&r<=t)return make_pair(tr[p],pos[p]); int mid=(l+r)>>1; pushdown(p); if(t<=mid)return query(ls,l,mid,s,t); if(s>mid)return query(rs,mid+1,r,s,t); auto lans=query(ls,l,mid,s,t),rans=query(rs,mid+1,r,s,t); if(lans.first>rans.first)return lans; else return rans; } signed main(){ scanf("%lld",&n),memset(tr,-0x3f,sizeof(tr)); for(int i=1;i<=n;i++){ int x,y,w; scanf("%lld%lld%lld",&x,&y,&w); lsh[++tot]=x,lsh[++tot]=y; a[i]=(node){min(x,y),max(x,y),w}; }sort(lsh+1,lsh+tot+1),tot=unique(lsh+1,lsh+tot+1)-lsh-1; for(int i=1;i<=n;i++){ a[i].l=lower_bound(lsh+1,lsh+tot+1,a[i].l)-lsh; a[i].r=lower_bound(lsh+1,lsh+tot+1,a[i].r)-lsh; q[a[i].l].push_back(make_pair(a[i].r,a[i].w)),m=max(m,a[i].r); } build(1,1,m); for(int i=m;i;i--){ for(auto t:q[i])update(1,1,m,t.first,m,t.second); auto t=query(1,1,m,i,m);t.first+=lsh[i]; if(t.first>ans)ans=t.first,L=lsh[i],R=lsh[t.second]; } if(ans<0)ans=0,L=lsh[m]+1,R=lsh[m]+1; printf("%lld\n%lld %lld %lld %lld",ans,L,L,R,R); return 0; } ``` - Ptz Winter 2021 Day 5 C. Multiple? - 给定 $n,k$,求出长度为 $n-k$ 的值域在 $[1,n]$ 且所有非空连续子段和都不是 $n$ 的倍数的整数数列 $a$ 的数量。$k< n/4< n<998244353$。 子序列的和很难刻画,我们考虑限制更松的情况:子段的和。 自然地想到前缀和来刻画子段和。我们一开始有了 $n-k+1$ 个前缀和(包括前缀 $0$),那么考虑**交换**两个数,我们能构造多少不同前缀和?是 $n-k+1+m$ 个,其中 $m$ 表示我最多能将原序列分成多少组使得每组内两个数且不同。那么 $m=\min(\left\lfloor\frac{n-k}{2}\right\rfloor,n-k-t)$,其中 $t$ 表示众数 $x$ 出现次数。根据抽屉原理有 $n-k+1+m<n$,所以 $m<k<n/4$,易得 $t>\left\lfloor\frac{n}{2}\right\rfloor$。 注意到若 $\gcd(n,x)>1$ 则必定可以找到 $n/\gcd(n,x)$ 个 $x$ 使它们模 $n$ 为 $0$,所以 $\gcd(n,x)=1$,那么 $x$ 就有逆元,将所有数乘上 $x^{-1}$,此时就有了 $t$ 个 $1$。而现在与原题目限制之间还差一个条件:所有数之和 $<n$,若满足以上所有条件便是充要的。 若 $S=\sum a>n$ 则剩下的数和 $S-t>n-t$ 必定存在可以和 $t$ 个 $1$ 配对凑出 $n$ 的数;对于充分性显然。 所以原题目便转变为要求至少 $n/2$ 个 $1$,总和 $<n$ 的有序序列数,使用插板法得 $\dbinom{n-1}{k-1}$。再乘上 $x$ 共 $\phi(n)$ 种取值。时间复杂度 $O(k)$。 ```cpp #include <bits/stdc++.h> using namespace std; #define int long long const int mod=998244353; int n,k,fac=1,ifac=1,nn,phi=1; int qpow(int a,int b){ int res=1; while(b){ if(b&1)res=res*a%mod; a=a*a%mod; b>>=1; } return res; } signed main(){ scanf("%lld%lld",&n,&k),nn=n; for(int i=2;i*i<=n;i++)if(n%i==0){ phi=phi*(i-1),n/=i; while(n%i==0)n/=i,phi*=i; }if(n>1)phi=phi*(n-1);n=nn; for(int i=n-k+1;i<n;i++)fac=fac*i%mod; for(int i=1;i<k;i++)ifac=ifac*i%mod;ifac=qpow(ifac,mod-2); printf("%lld",phi*fac%mod*ifac%mod); return 0; } ``` - CF1246F Cursor Distance - 给定一个长度为 $n$ 的由小写字母组成的字符串 $s$,当光标在 $x$ 位置时,一次合法的操作定义为选取 $(c,0/1)$ 分别表示一个字符和方向(左/右),执行该操作会使光标移动到 $x$ 对应方向的 $c$ 字符的最接近的的出现位置(要求必须存在)。定义 $dis(i,j)$ 为光标初始在 $i$ ,使光标移动到 $j$ 的最小操作次数。求 $\displaystyle \sum_{i=1}^n \sum_{j=1}^n dis(i,j)$。$n\le 10^5$。 我们考虑刻画 $dis(i,j)$ 这个东西。从 $i$ 开始跳 $j$ 步所能跳到的点不一定形成一个区间,正难则反,发现跳 $j$ 步到 $i$ 的点集是一个区间,我们用 $[l_{i,j},r_{i,j}]$ 来刻画。 首先答案可以被改写成 $\displaystyle \sum_{i=1}^n \sum_k n-1-(r_{i,k}-l_{i,k})$,那么转化为快速求出 $l,r$,一个自然的想法是倍增。然而,写出式子:$[l_{i,k+1},r_{i,k+1}]=\bigcup_{l_{i,k}\le j\le r_{i,k}}[l_{j,0},r_{j,0}]$。 看起来 $l,r$ 是互相影响的而不能倍增,我们进行更仔细的观察:设 $[l_{i,k},r_{i,k}]$ 中每种字符首次出现位置从小到大排序后是 $pos_{1},pos_{2},\dots pos_{m}$,那么 $l_{i,k+1}$ 被 $\min l_{pos_j,0}$ 更新。格式化地:$l_{i,k+1}=\min_{j\le 26}l_{pos_j,0}$,其中对于 $[l_{i,k},r_{i,k}]$ 中未出现的字符设为 $n$,注意 $pos$ 仍然是按照从小到大排序的。 那么,若当前字符集维护的 $pos$ **不变**,那么 $l,r$ 各自独立,便可以各自**倍增**得出 $l^k,r^k$。再注意到我们关心的只是区间中字符集大小而不是具体的集合,那么字符集大小 $|\Sigma|$ 的变化量只有 $26$,我们枚举 $|\Sigma|$ 的大小分别算出此时的 $l_{i,k},r_{i,k}$,判断区间是否具有 $|\Sigma|$ 个字符即可。复杂度 $O(26n\log n)$。 ```cpp #pragma GCC optimize("Ofast") #include<bits/stdc++.h> using namespace std; #define int long long const int N=1e5+6; int n,l[N],r[N],L[N][27],R[N][27],pre[N][27],nxt[N][27],lst[27],f[18][N],g[18][N],F[18][N],G[18][N],ans; char s[N]; signed main(){ scanf("%s",s+1),n=strlen(s+1); for(int i=0;i<26;i++)lst[i]=1; for(int i=1;i<=n;i++){ l[i]=lst[s[i]-'a'],lst[s[i]-'a']=i; memcpy(pre[i],lst,sizeof(pre[i])),sort(pre[i],pre[i]+26,greater<int>()); }for(int i=0;i<26;i++)lst[i]=n; for(int i=n;i>=1;i--){ r[i]=lst[s[i]-'a'],lst[s[i]-'a']=i; memcpy(nxt[i],lst,sizeof(nxt[i])),sort(nxt[i],nxt[i]+26),nxt[i][26]=n+1; } for(int i=1;i<=n;i++){ L[i][0]=R[i][0]=i; for(int j=0;j<26;j++)L[i][j+1]=min(L[i][j],l[nxt[i][j]]),R[i][j+1]=max(R[i][j],r[pre[i][j]]); } for(int i=1;i<=n;i++)l[i]=r[i]=i; for(int t=1;t<=26;t++){ for(int i=1;i<=n;i++)f[0][i]=L[i][t],g[0][i]=R[i][t],F[0][i]=G[0][i]=i; for(int j=1;(1<<j)<=n;j++)for(int i=1;i<=n;i++){ f[j][i]=f[j-1][f[j-1][i]],g[j][i]=g[j-1][g[j-1][i]]; F[j][i]=F[j-1][i]+F[j-1][f[j-1][i]],G[j][i]=G[j-1][i]+G[j-1][g[j-1][i]]; } for(int i=1;i<=n;i++){ for(int j=31-__builtin_clz(n);j>=0;j--){ int nl=f[j][l[i]],nr=g[j][r[i]]; if(nr<nxt[nl][t]){ ans+=((n-1)<<j)-(G[j][r[i]]-F[j][l[i]]); l[i]=nl,r[i]=nr; } } if(r[i]<nxt[l[i]][t]){ ans+=n-1-(r[i]-l[i]); l[i]=L[l[i]][t],r[i]=R[r[i]][t]; } } } printf("%lld",ans); return 0; } ``` - CF1383C String Transformation 2 - 给定两个字符串 $A$ 和 $B$,每次可以选取 $A$ 中**若干个**相同的字符 $x$,然后将其改成 $y$,求使得 $A=B$ 的最小操作次数。$\sum |A|,\sum |B|\le 10^5$,$A$ 和 $B$ 仅由 ```a``` $\sim$ ```t``` 构成(共计 $20$ 种字符)。 删去已经相同的位置,然后每对 $a_i,b_i$,那么看作 $a_i$ 向 $b_i$ 连了一条有向边,显然每个弱连通块独立,所以对于每个弱连通块讨论。 对于一个 DAG,答案为点数减一。考虑怎么构造:对于 DAG 来分层,对于跨层的边实际上没有用所以可以删掉,那么每一层的点之间连边,然后上一层最后一个点联想这一层第一个点,不难发现这也是最小的方式,因为形成的就是一棵生成树。 对于有环的图,我们可以花两步减少一个点,具体地便是假如要删掉 $u$,第一步是选一个 $v$,把 $u$ 变成 $v$,最后一步是把 $v$ 变成 $u$。这样连向 $u$ 的边都可以看做连向 $v$ 的,$u$ 连出去的点都可以看做 $v$ 连出去的。 所以我们可以一直删点只剩下一个 DAG,因为 DAG 的答案是确定且最优的;那么我们肯定是剩下最大的点集 $S$ 使得 $S$ 的导出子图是 DAG。所以答案的一个下界就是 $(n-|S|)\times 2+(|S|-1)=2n-|S|-1$。 事实上这也是最优解。证明我们先考虑将结论转化为:对于以个弱连通块可以用 $k$ 步解决,那么一定存在点集 $S$ 使得 $2n-k-1\le |S|$ 且 $S$ 导出子图为 DAG。 考虑维护 $S$ 和所有 DAG 的和 $T$,初始时每个点自身就是一个 DAG,考虑一直加边,当加入 $(u,v)$: 1. 若 $(u,v)$ 在同一个连通块则 $v$ 从 $S$ 中删去且 $T$ 减一,连通块数 $c$ 不变。 2. 若不在一个连通块,则 $T$ 不变且 $c$ 减一。 那么令该连通块点数为 $n$,其中最多 $n-1$ 步删连通块数 $c$,所以至多 $k-(n-1)$ 次减 $T$,那么 $n-(k-(n-1))\le|S|$ 即 $2n-k-1\le |S|$。 所以结论成立,答案就是 $20\times 2-|S|-c$,找到最大的点集 $S$ 使得 $S$ 的导出子图是 DAG 使用状压 dp 即可。复杂度 $O(n+2^{20}\times 20)$。 还有这题*3100评紫/kx ```cpp #include<bits/stdc++.h> using namespace std; const int N=1e5+6; int T,n,fa[20],g[20],dp[1<<20],ans,sz; char s[N],t[N]; int find(int x){return fa[x]^x?fa[x]=find(fa[x]):x;} void solve(){ scanf("%d%s%s",&n,s,t),sz=0; for(int i=0;i<20;i++)fa[i]=i,g[i]=0; for(int i=0;i<(1<<20);i++)dp[i]=0; for(int i=0;i<n;i++){ g[s[i]-'a']|=1<<(t[i]-'a'); fa[find(s[i]-'a')]=find(t[i]-'a'); } dp[0]=1,ans=40; for(int i=0;i<20;i++)if(i==find(i))ans--; for(int s=0;s<(1<<20);s++)if(dp[s]){ for(int i=0;i<20;i++)if(!(s&(1<<i))&&!(s&g[i]))dp[s|(1<<i)]=1; sz=max(sz,__builtin_popcount(s)); } printf("%d\n",ans-sz); return; } int main(){ scanf("%d",&T); while(T--)solve(); return 0; } ``` - PTZ summer 2021 Day3 I. Intellectual Implementation - 给定 $n$ 个长方形 $T_i$,问有几个三元组 $i<j<k$ 满足 $T_i,T_j,T_k$ 两两无交。$n\le 2\times 10^5$。保证长方形的 $l,r,u,d$ 两两不同。 容斥,用 $\dbinom n 3$ 减去不合法的方案。对于不合法的情况分类: 1. 两个有交且都和另一个无交 2. 两个有交其中一个和剩下一个有交另一个无交 3. 两两有交 将上述三种情况的方案数分别设为 $c_1,c_2,c_3$,设 $cnt_i$ 表示和第 $i$ 个矩形有交的矩形个数,那么我们有: $$\sum_i cnt_i(n-2)=2c_1+4c_2+6c_3$$ $$\sum_i \binom{cnt_i} 2=c_2+3c_3$$ 所以我们可以直接得出 $c_1+c_2=\sum\frac{cnt_i(n-1-cnt_i)}{2}$,至于对于 $cnt_i$ 的计算使用线段树即可。 那么考虑计算 $c_3$,枚举三个矩形中 $u_i$ 最小的,剩下的矩形要保证和它有交等价于 $d_j<u_i<u_j$,那么刻画为在 $d_j$ 插入 $[l_j,r_j]$,在 $u_j$ 删去,然后查询区间中有多少 $j,k$ 使得 $[l_i,r_i],[l_j,r_j],[l_k,r_k]$ 两两有交,这就把问题拍到一维上了。 那么计算这个再次容斥,变为选择两个区间有交的方案数减去和 $[l_i,r_i]$ 有交而这两个区间无交的方案数。前者容易计算,后者等价于 $l_i<r_j<l_k<r_i$,那么使用线段树维护每个区间内有多少个区间左/右端点即可。复杂度 $O(n\log n)$。 ```cpp #include<bits/stdc++.h> using namespace std; #define ls p<<1 #define rs p<<1|1 #define ll long long const int N=2e5+6; int n,tot,cnt[N],lsh[N<<1]; ll ans; struct node{int x[4],i;}a[N],b[N]; namespace AddCnt{ int tr[N<<1]; int lowbit(int x){return x&-x;} void add(int x){while(x<=tot)tr[x]++,x+=lowbit(x);return;} int qry(int x){int res=0;while(x)res+=tr[x],x-=lowbit(x);return res;} void clear(){memset(tr,0,sizeof(tr));return;} void solve(int p){ if(p&1)for(int i=1;i<=n;i++)a[i].x[p]=tot-a[i].x[p]+1,a[i].x[p^1]=tot-a[i].x[p^1]+1; sort(a+1,a+n+1,[&](node x,node y){return x.x[p]<y.x[p];}); for(int i=1;i<=n;i++){ cnt[a[i].i]+=qry(a[i].x[p]); add(a[i].x[p^1]); } if(p&1)for(int i=1;i<=n;i++)a[i].x[p]=tot-a[i].x[p]+1,a[i].x[p^1]=tot-a[i].x[p^1]+1; clear();return; } } struct Node{ int l,r;ll t; Node operator+(const Node&rhs)const{return{l+rhs.l,r+rhs.r,t+rhs.t+1ll*r*rhs.l};} }tr[N<<3]; void pushup(int p){ tr[p]=tr[ls]+tr[rs]; return; } void build(int p,int l,int r){ tr[p].t=0ll,tr[p].l=tr[p].r=0; if(l==r)return; int mid=(l+r)>>1; build(ls,l,mid),build(rs,mid+1,r); return; } void addL(int p,int l,int r,int ps,int val){ if(l==r){tr[p].l+=val;return;} int mid=(l+r)>>1; if(ps<=mid)addL(ls,l,mid,ps,val); else addL(rs,mid+1,r,ps,val); pushup(p); return; } void addR(int p,int l,int r,int ps,int val){ if(l==r){tr[p].r+=val;return;} int mid=(l+r)>>1; if(ps<=mid)addR(ls,l,mid,ps,val); else addR(rs,mid+1,r,ps,val); pushup(p); return; } auto qry(int p,int l,int r,int s,int t){ if(s<=l&&r<=t)return tr[p]; int mid=(l+r)>>1; if(t<=mid)return qry(ls,l,mid,s,t); if(s>mid)return qry(rs,mid+1,r,s,t); return qry(ls,l,mid,s,t)+qry(rs,mid+1,r,s,t); } namespace SubCnt{ void solve(int p){ if(p&1)for(int i=1;i<=n;i++)a[i].x[p]=tot-a[i].x[p]+1,a[i].x[p^1]=tot-a[i].x[p^1]+1; build(1,1,tot); for(int i=1;i<=n;i++)b[i]=a[i]; sort(a+1,a+n+1,[&](node x,node y){return x.x[p]<y.x[p];}); sort(b+1,b+n+1,[&](node x,node y){return x.x[p^1]<y.x[p^1];}); for(int i=1,j=1;i<=n;i++){ while(j<=n&&b[j].x[p^1]<a[i].x[p])addL(1,1,tot,b[j].x[2],1),addR(1,1,tot,b[j].x[3],1),j++; cnt[a[i].i]-=qry(1,1,tot,1,a[i].x[2]).r+qry(1,1,tot,a[i].x[3],tot).l; } if(p&1)for(int i=1;i<=n;i++)a[i].x[p]=tot-a[i].x[p]+1,a[i].x[p^1]=tot-a[i].x[p^1]+1; return; } } void C3(){ build(1,1,tot); for(int i=1;i<=n;i++)b[i]=a[i]; sort(a+1,a+n+1,[&](node x,node y){return x.x[0]<y.x[0];}); sort(b+1,b+n+1,[&](node x,node y){return x.x[1]<y.x[1];}); for(int i=1,j=1;i<=n;i++){ while(j<=n&&b[j].x[1]<a[i].x[0])addL(1,1,tot,b[j].x[2],-1),addR(1,1,tot,b[j].x[3],-1),j++; int t=tr[1].l-qry(1,1,tot,1,a[i].x[2]).r-qry(1,1,tot,a[i].x[3],tot).l; ans+=t*(t-1ll)/2ll-qry(1,1,tot,a[i].x[2],a[i].x[3]).t; addL(1,1,tot,a[i].x[2],1),addR(1,1,tot,a[i].x[3],1); } return; } int main(){ scanf("%d",&n); for(int i=1;i<=n;i++){ for(int j=0;j<4;j++)scanf("%d",&a[i].x[j]); a[i].i=i; } for(int j=0;j<4;j+=2){ tot=0; for(int i=1;i<=n;i++)lsh[++tot]=a[i].x[j],lsh[++tot]=a[i].x[j+1]; sort(lsh+1,lsh+tot+1); for(int i=1;i<=n;i++)a[i].x[j]=lower_bound(lsh+1,lsh+tot+1,a[i].x[j])-lsh,a[i].x[j+1]=lower_bound(lsh+1,lsh+tot+1,a[i].x[j+1])-lsh; } for(int p=0;p<4;p++)AddCnt::solve(p); SubCnt::solve(0),SubCnt::solve(1); for(int i=1;i<=n;i++)ans+=cnt[i]*(n-1ll-cnt[i]); ans/=2ll,C3(); printf("%lld",1ll*n*(n-1)*(n-2)/6-ans); return 0; } ``` - [ARC093F] Dark Horse - $2^n$ 个人进行比赛,以满二叉树的方式进行淘汰赛。你是编号 $1$,你只打不过 $a_1<a_2,\dots a_m$ 编号的人,对于剩下的你都打得过。对于没有你参与的一餐比赛胜负定义编号小的人赢。求所有 $(2^n)!$ 种位置的排列中你赢的情况之和。$n,m\le 16$。 首先你的位置不重要,不妨设为 $1$ 然后最后答案乘上 $2^n$。 那么这个淘汰赛中你会参与的区间便是形如线段树的节点:$\forall k,[2^{k-1}+1,2^k]$。那么你会输当且仅当这 $n$ 个区间中有一个区间的最小值为 $a$ 中的人。 直接计算不太行,自然地进行容斥,设 $f_{i,s}$ 表示考虑了 $a_1\sim a_i$,$s$ 是二进制下的状态表示第 $k$ 个区间的最小值已经被钦定为 $a_1\sim a_i$ 中的某个值了。转移显然枚举 $a_{i+1}$,但是由于 $a_i\sim a_{i+1}$ 中不知道有多少数使用过了,难以转移。 注意我们只关心比当前 $a_i$ 小的值,所以将 $a$ reverse 后再进行上述 dp 即可。转移时候,所有被填入 $s$ 中的区间的数都是 $\ge a_i$ 的数,不会影响 $a_i$。枚举当前的 $a$ 放进哪个长度为 $2^k$ 的区间,填充这个区间的数便在 $(a_i,2^n]$ 中选择,注意要减去被选择了的 $s$ 个数,所以转移系数便是 $\dbinom{2^n-a_i-s}{2^k-1}\times (2^k)!$,时间复杂度 $O(n^22^n)$。 ```cpp #include<bits/stdc++.h> using namespace std; #define int long long const int N=18; const int S=1<<17; const int mod=1e9+7; int n,m,a[N],fac[S],ifac[S],pw[N],dp[N][S],ans; int qpow(int a,int b){ int res=1; while(b){ if(b&1)res=res*a%mod; a=a*a%mod; b>>=1; } return res; } void init(){ fac[0]=ifac[0]=pw[0]=1; for(int i=1;i<S;i++)fac[i]=fac[i-1]*i%mod; for(int i=1;i<N;i++)pw[i]=pw[i-1]*2%mod; ifac[S-1]=qpow(fac[S-1],mod-2); for(int i=S-2;i;i--)ifac[i]=ifac[i+1]*(i+1)%mod; return; } int C(int n,int m){ if(n<m||n<0||m<0)return 0; return fac[n]*ifac[m]%mod*ifac[n-m]%mod; } signed main(){ scanf("%lld%lld",&n,&m),init(); for(int i=1;i<=m;i++)scanf("%lld",&a[i]); reverse(a+1,a+m+1),dp[0][0]=1; for(int i=0;i<m;i++)for(int s=0;s<pw[n];s++){ for(int k=0;k<n;k++)if(!(s&(1<<k))) dp[i+1][s|(1<<k)]=(dp[i+1][s|(1<<k)]+dp[i][s]*C(pw[n]-a[i+1]-s,pw[k]-1)%mod*fac[pw[k]]%mod)%mod; dp[i+1][s]=(dp[i+1][s]+dp[i][s])%mod; } for(int s=0;s<pw[n];s++){ int fl=__builtin_popcount(s)&1; ans=(ans+(fl?-1:1)*dp[m][s]*fac[pw[n]-1-s]%mod+mod)%mod; } printf("%lld",ans*pw[n]%mod); return 0; } ``` - AtCoder Code Festival 2017 qual B E-Popping Balls - 有 $A+B$ 个球排成一行,其中左边 $A$ 个是红色的,右边 $B$ 个是蓝色的,你可以选定一对整数 $(s,t)$($s<t$),然后重复进行以下操作直到球被取完:将第一个球或第 $s$ 个球或第 $t$ 个球(当前球的数量必须够才能执行)拿出来。求最后拿出来的球的颜色序列有多少种不同方案数。答案对 $10^9+7$ 取模。方案不同当且仅当存在某一次取出的球的颜色不同。$A, B \leq 2000$。 注意到这个 $s$ 其实很尴尬。当 $s$ 位于红球时我们可以选择 $1$,位于蓝球时选择 $t$,都可以保证当前选择的颜色不变,所以 $s$ 只可能在球总数 $<t$ 后选择。 所以现在我们只考虑选择 $t$。和 $s$ 上述的结论一样,那么 $t$ 只可能选择蓝球。那么 $t$ 选择一行中第一个蓝球一定不劣,且这样不会算重,因为最终序列中蓝球第一次出现的位置不同方案必定不同,且其位置显然在区间 $[1,A+1]$ 中所以也不会漏。此时我们钦定 $t$ 为取走第一个蓝球的位置。 所以我们最终序列一定是由 $A+1-t$ 个红球开始然后接着一个蓝球。我们枚举 $t\in[1,A+1-t]$ 后剩下了 $t-1$ 和红球和 $B-1$ 个蓝球。此时我们还需要取走 $B-1$ 个球才能轮到 $s$ 的回合。 注意此时剩下的红蓝球没有限制,所以假设最后 $B-1$ 个球中有 $i$ 个蓝球,那么我们就需要取走 $i$ 个红球和 $B-1-i$ 个蓝球,方案数为 $\dbinom{B-1}i$。 现在终于来到 $s$ 的回合,剩下 $t-1-i$ 个红球和 $i$ 个蓝球,做法和 $t$ 的回合一模一样,也是形如钦定 $s$ 为剩下第一个蓝球然后巴拉巴拉之类的。 此时我们的复杂度是 $O(n^4)$($s,t$ 的回合各需要枚举 $n^2$ 级别),但是将式子写出来是形如 $$\sum_t\sum_i\dbinom{B-1}i \sum_s\sum_j \dbinom{i-1}j$$ 那么后面两个 $\sum$ 就可以二次前缀和优化了,复杂度 $O(n^2)$。 ```cpp #include<bits/stdc++.h> using namespace std; #define int long long const int N=2e3+4; const int mod=1e9+7; int a,b,C[N][N],f[N][N],g[N][N],ans; void init(int n){ for(int i=0;i<=n;i++){ C[i][0]=g[i][0]=f[i][0]=1; for(int j=1;j<=i;j++)C[i][j]=(C[i-1][j]+C[i-1][j-1])%mod; for(int j=1;j<=n;j++)f[i][j]=(f[i][j-1]+C[i][j])%mod,g[i][j]=(g[i][j-1]+f[i][j])%mod; } return; } main(){ scanf("%lld%lld",&a,&b),init(N-2); for(int t=1;t<=a+1;t++){ for(int i=0;i<t;i++){ if(!i)ans++; else ans=(ans+g[i-1][t-i-1]*C[b-1][i]%mod)%mod; } } printf("%lld",ans); } ``` - Gym104337-D Darkness II - 网格平面上有 $n$ 个格子是黑色的。每秒钟,若一个白色格子和两个及以上的黑色格子四相邻,它会变成黑色,求当不再有格子变色时,黑色格子的个数。$n\le 10^5$。 显然最终局面下一定是形成若干个不交的黑色矩形。合并的过程就是两个黑色矩形合并。我们考虑两个黑色矩形什么情况下会合并。不难发现若存在点 $(x,y)$ 使得其距离某个黑色矩形的曼哈顿距离 $\le 2$,那么黑色矩形就是并上 $(x,y)$,这里的合并就是 `xl,xr,yl,yr` 取 $\min$ 取 $\max$,也就是合并为新的大黑色矩形。 所以初始的黑色点看作大小为 $1\times 1$ 的矩形即可,然后查询随便一个和它有交的矩形然后合并。问题转化为二维平面上若干矩形,求两两合并后最后的所有矩形。 ~~有个小可爱不会这个经典的 trick,怎么回事呢?~~ 我们按照 `xr` 排序,对于 $y$ 轴离散化后建立线段树,每次查询一段区间 $[l,r]$ 内满足矩形右端点大于左端点的矩形编号。 注意我们按照 `xr` 排序了,所以加入的矩形可以看作一个栈,每次拿出栈顶比较是否 $\ge xl$ 即可。然后考虑怎么处理与 $[l,r]$ **有交即可**这个限制呢?(因为只是要求两个矩形之间有交而不是包含) 建立两棵线段树,我们每次修改时插入区间 $[yl,yr]$,在第一棵线段树上所以遍历到的区间都把区间编号 push 进去,当访问到某一个区间满足 $yl\le l$ 且 $r\le yr$ 时在第二棵线段树中把区间编号 push 进去(此时第一棵不 push)。 对于查询时,我们反过来,访问到一个节点就查询第二棵,遇到完全被包含的区间就查询第一棵即可。时间复杂度是优秀的 $O(n\log n)$。很好写。 ```cpp #include<bits/stdc++.h> using namespace std; #define ls p<<1 #define rs p<<1|1 #define int long long const int N=1e6+7; int n,m,lsh[N],ans; pair<int,int>a[N]; vector<array<int,4>>rect; vector<int>f[N],g[N],vis; void update(int p,int l,int r,int s,int t,int i){ if(t<l||s>r)return; f[p].push_back(i); if(s<=l&&r<=t){ g[p].push_back(i); return; } int mid=(l+r)>>1; update(ls,l,mid,s,t,i),update(rs,mid+1,r,s,t,i); return; } int query(int p,int l,int r,int s,int t,int xl){ if(t<l||s>r)return -1; while(!f[p].empty()&&vis[f[p].back()])f[p].pop_back(); while(!g[p].empty()&&vis[g[p].back()])g[p].pop_back(); if(!g[p].empty()&&rect[g[p].back()][1]>=xl)return g[p].back(); if(s<=l&&r<=t){ if(!f[p].empty()&&rect[f[p].back()][1]>=xl)return f[p].back(); return -1; } int mid=(l+r)>>1,res=query(ls,l,mid,s,t,xl); if(~res)return res;return query(rs,mid+1,r,s,t,xl); } signed main(){ scanf("%lld",&n); for(int i=1;i<=n;i++){ int x,y;scanf("%lld%lld",&x,&y); a[i]=make_pair(x,y); lsh[++m]=y-1,lsh[++m]=y,lsh[++m]=y+1,lsh[++m]=y+2; }sort(a+1,a+n+1),sort(lsh+1,lsh+m+1),m=unique(lsh+1,lsh+m+1)-lsh-1; for(int i=1;i<=n;i++){ int xl=a[i].first,xr=xl,yl=a[i].second,yr=yl; yl=lower_bound(lsh+1,lsh+m+1,yl)-lsh,yr=lower_bound(lsh+1,lsh+m+1,yr)-lsh; while(1){ int t=query(1,1,m,yl-2,yr+2,xl); if(t==-1)t=query(1,1,m,yl,yr,xl-2); if(t==-1)t=query(1,1,m,yl-1,yr+1,xl-1); if(t==-1)break; xl=min(xl,rect[t][0]),yl=min(yl,rect[t][2]),yr=max(yr,rect[t][3]); vis[t]=1; } update(1,1,m,yl,yr,rect.size()),rect.push_back({xl,xr,yl,yr}),vis.push_back(0); } for(int i=0;i<rect.size();i++)if(!vis[i])ans+=(rect[i][1]-rect[i][0]+1)*(lsh[rect[i][3]]-lsh[rect[i][2]]+1); printf("%lld\n",ans); return 0; } ``` - [AGC039F] Min Product Sum - 有一个大小为 $n \times m$ 的矩阵。矩阵中每个数的取值都是 $[1, K]$。于一个矩阵,定义函数 $f(x,y)$ 为:第 $x$ 行和第 $y$ 列的一共 $n+m - 1$ 个数中的最小值。对于一个矩阵,定义其权值为 $\prod_{x=1}^{n}\prod_{y=1}^{m}f(x,y)$。求出所有 $K^{nm}$ 种矩阵,每个矩阵的权值和。$1 \leq n,m,k \leq 100$,$10^8 \leq D \leq 10^9$。 将权值刻画为组合意义上,就是一个矩阵 $B$ 满足 $B_{i,j}\le \min(\min_k A_{i,k},\min_k{A_{k,j}})$,进一步转化就是 $B$ 第 $i$ 行的 $\max\le A$ 第 $i$ 行的 $\min$,列同理。权值和就是合法 $(A,B)$ 数量。 对于这种权值最大/最小值的限制,我们容易想到将数值将小到大插入。假设当前插入数值为 $k$ 个点。设 dp 状态:$f_{k,i,j}$ 表示当前填入的所有点数值都 $\le k$ 且有 $i$ 个 $B$ 行被钦定最大值,$j$ 个 $A$ 列被钦定最小值。 我们考虑 $B$ 中新增一个行的最大值 $k$ 和新增 $A$ 中一个列的最小值 $k$,转移分为两步: 1. 新增 $B$ 的一行,考虑这个行上的每一个位置,如果对应的列上 $A$ 的最小值已经确定(且一定 $<k$),那么为了适配限制,我们往这个位置的 $A$ 上填入一个 $≥k$ 的数;如果对应的列上 $A$ 的最小值还未确定,那么可以在 $B$ 中填入一个 $≤k$ 的数,并且要求这些数中至少有一个 $=k$。方案数就是 $(K-k+1)^{j}(k^{m-j}-(k-1)^{m-j})$。 2. 新增 $A$ 的一列。与上述同理。 实现时 dp 状态增加两维来表示当前在转移的第 $1/2$ 步,注意将 dp 的转移系数预处理。时间复杂度 $O(n^4)$。 ```cpp #include<bits/stdc++.h> using namespace std; const int N=120; int n,m,K,mod,C[N][N],coef[N][N][N][2],dp[N][N][N][2]; int qpow(int a,int b){ int res=1; while(b){ if(b&1)res=1ll*res*a%mod; a=1ll*a*a%mod; b>>=1; } return res; } void init(int n){ C[0][0]=1; for(int i=1;i<=n;i++){ C[i][0]=1; for(int j=1;j<=i;j++)C[i][j]=(C[i-1][j]+C[i-1][j-1])%mod; } return; } int main(){ scanf("%d%d%d%d",&n,&m,&K,&mod),init(N-2); for(int k=1;k<=K;k++)for(int i=0;i<=m;i++){ int t=1ll*qpow(K-k+1,i)*((qpow(k,m-i)-qpow(k-1,m-i)+mod)%mod)%mod; coef[k][i][0][0]=1; for(int j=1;j<=n;j++)coef[k][i][j][0]=1ll*coef[k][i][j-1][0]*t%mod; } for(int k=1;k<=K;k++)for(int i=0;i<=n;i++){ int t=1ll*qpow(k,n-i)*((qpow(K-k+1,i)-qpow(K-k,i)+mod)%mod)%mod; coef[k][i][0][1]=1; for(int j=1;j<=m;j++)coef[k][i][j][1]=1ll*coef[k][i][j-1][1]*t%mod; } dp[1][0][0][0]=1; for(int k=1;k<=K;k++)for(int i=0;i<=n;i++)for(int j=0;j<=m;j++){ for(int t=0;t<=n-i;t++) dp[k][i+t][j][1]=(dp[k][i+t][j][1]+1ll*dp[k][i][j][0]*C[n-i][t]%mod*coef[k][j][t][0]%mod)%mod; for(int t=0;t<=m-j;t++) dp[k+1][i][j+t][0]=(dp[k+1][i][j+t][0]+1ll*dp[k][i][j][1]*C[m-j][t]%mod*coef[k][i][t][1]%mod)%mod; } printf("%d",dp[K+1][n][m][0]); return 0; } ``` - CF653G Move by Prime - 一个长度为 $n$ 的数列 $a_i$,你可以进行任意次操作:将其中一个数乘上或者除以一个质数。使得最终所有数相同,并使得操作数尽可能小。求所有子序列的操作数之和是多少。$1 \le n,a_i \leq 3 \times 10^5$。 将每个质数独立计算贡献,对于一个子序列中质数指数的序列 $x$,设我们最终全部被调整为 $mid$,代价就是 $\sum|x_i-mid|$,显然 $mid$ 应该取 $x$ 的中位数。不难发现 $mid$ 的贡献最终会被消去,来考虑每个 $x_i$ 产生的贡献。 假设排名为 $i$ 的 $x_i$ 左边选择了 $a$ 个在子序列中,右边选择了 $b$ 个,那么不难发现贡献为:当 $a<b$ 为 $x_i$,$a>b$ 为 $-x_i$,否则为 $0$。 发现我们只关系 $a-b$ 的大小而不是 $a,b$ 各自的值。但是直接枚举 $a-b$ 会因为没有组合意义而算重。一种常见二点套路是设 $j=i-1-a+b$,表示左边没有选择进子序列的个数减去右边加入子序列的个数,这样就出现组合意义。我们得到 $x_i$ 的贡献: $$x_i(\sum_{j=i}^{n-1}\binom{n-1}j-\sum_{j=1}^{i-2}\binom{n-1}j)$$ 所以预处理 $\binom{n-1}j$ 的前缀和,然后再处理出 $\sum_{j=i}^{n-1}\binom{n-1}j-\sum_{j=1}^{i-2}\binom{n-1}j$ 的前缀和,可以做到 $O(n)$ 计算。 对于枚举质数以及 $x_i$ 的部分使用类似与埃式筛暴力枚举即可,复杂度 $O(V\log \log V)$。 ```cpp #include<bits/stdc++.h> using namespace std; #define int long long const int N=3e5+6; const int mod=1e9+7; int n,fac[N],ifac[N],c[N],f[N],ans,rk[N],cnt[N],vis[N],a[N]; int qpow(int a,int b){ int res=1; while(b){ if(b&1)res=res*a%mod; a=a*a%mod; b>>=1; } return res; } int C(int n,int m){ if(n<0||m<0||n<m)return 0; return fac[n]*ifac[m]%mod*ifac[n-m]%mod; } void init(int n){ fac[0]=ifac[0]=1; for(int i=1;i<=n;i++)fac[i]=fac[i-1]*i%mod; ifac[n]=qpow(fac[n],mod-2); for(int i=n-1;i;i--)ifac[i]=ifac[i+1]*(i+1)%mod; c[0]=C(n-1,0);for(int i=1;i<n;i++)c[i]=(c[i-1]+C(n-1,i))%mod; for(int i=1;i<=n;i++)f[i]=(f[i-1]+c[n-1]-c[i-1]-c[i-2]+mod+mod)%mod; return; } signed main(){ scanf("%lld",&n),init(n); for(int i=1;i<=n;i++)scanf("%lld",&a[i]),cnt[a[i]]++; for(int i=2;i<N;i++)if(!vis[i]){ for(int j=i;j<N;j+=i)vis[j]=1; int k=1; for(int j=i;j<N;j*=i,k++){ rk[k]=0; for(int t=j;t<N;t+=j)rk[k]+=cnt[t]; if(!rk[k])break; }rk[k]=0; for(int j=k-1;j;j--)ans=(ans+j*(f[rk[j]]-f[rk[j+1]]+mod)%mod)%mod; } printf("%lld",ans); return 0; } ``` - CF1656F Parametric MST - 给定一张 $n$ 个点的无向完全图 $K_n(t)$ ,点 $i$ 和点 $j$ 之间的边的边权 $w_{i,j}(t)$ 为 $a_i*a_j+t*(a_i+a_j)$ ,其中 $t$ 为任意实数。定义 $f(t)$ 为 $K_n(t)$ 的最小生成树的边权和。判断 $f(t)$ 是否有最大值并输出。$n\le 2\times 10^5,a_i\le 10^6$, ~~我感觉这个 F 2600 比下面同一场的 H 3200 难,怎么回事呢?~~ 将 $a$ 排序,考虑无解的情况,也就是 $t$ 的系数恒正/负,所以分析每个点的连向。 因为是完全图,一个显然正确的贪心策略是 $[1,i]$ 的点连向 $a_n$,$[i+1,n]$ 的点连向 $a_i$。那么总代价就是 $$W=\sum_{j=1}^{i}a_ja_n+\sum_{j={i+1}}^{n}a_ja_1+t(\sum_{i=1}^na_i+(n-i)a_1+ia_n)$$ 注意到 $w_{i,j}(t)=(a_i+t)(a_j+t)-t^2$,这在 $t$ 固定时形式很好。 所以无解时 $\forall i,a_i+t>0$ 或者 $<0$,所以带入 $t$ 的系数得到无解的两种情况:$\sum_{i=2}^{n}a_i+(n-1)a_i>0$ 或者 $\sum_{i=2}^{n}a_i+(n-1)a_n<0$。 容易得到 $t\in[-a_n,a_1]$,上述 $w_{i,j(t)}$ 的变形形式很好启发我们枚举 $t$,那么引发一个关键的 observation:$t$ 的值一定为某个 $-a_i$。这也是自然的,观察 $W$ 的式子,当 $i$ 确定时这是一个关于 $t$ 的一次函数,而当 $t\in [-a_{i+1},-a_i]$ 时 $i$ 不变所以 $t$ 必然取到端点处才有最值。 所以枚举 $t$ 然后带入 $W$ 式子即可计算出答案。记得减去重边 ($a_1\to a_n$)。 ```cpp #include<bits/stdc++.h> using namespace std; #define int long long const int N=2e5+6; int T,n,a[N],s[N]; void solve(){ scanf("%lld",&n); for(int i=1;i<=n;i++)scanf("%lld",&a[i]); sort(a+1,a+n+1); for(int i=1;i<=n;i++)s[i]=s[i-1]+a[i]; if(s[n]-s[1]+a[1]*(n-1)>0||s[n-1]+a[n]*(n-1)<0){puts("INF");return;} int ans=-1e18; for(int i=1;i<=n;i++){ int res=a[n]*s[i]+a[1]*(s[n]-s[i])+(-a[i])*(s[n]+i*a[n]+(n-i)*a[1])-a[1]*a[n]-(-a[i])*(a[1]+a[n]); ans=max(ans,res); } printf("%lld\n",ans); return; } signed main(){ scanf("%lld",&T); while(T--)solve(); return 0; } ``` - CF1656H Equal LCM Subsets - 两个集合$A,B$,大小分别为$n,m$,你需要找两个非空子集 $S_A\subseteq A,S_B\subseteq B$,使得 $S_A$ 中的元素的 $\text{lcm}$ 和 $S_B$ 中的元素的 $\text{lcm}$ 相等。判断是否有解并输出任意一种。$1\leq n, m\leq 10^3,1\leq a_i,b_i\leq4\times10^{36}$。 将寻找子集转化为在原来的集合中删去一些数后满足条件。考虑怎么样的一个 $a_i$ 必定会被删去:当存在某个质数 $p$ 使得 $v_p(a_i)>\max_{j} v_p(b_j)$,其中 $v_{p}(x)$ 表示 $x$ 中 $p$ 的幂数。 由于 $a_i,b_i$ 太大无法分解质因数,考虑将形式转化,也就是 :$ \gcd_{j=1}^m\dfrac{a_i}{\gcd(a_i,b_j)}$,那么这个就容易判断了。 暴力维护就是维护当前集合剩下的数然后每次暴力判断当前这个数是否需要删除,正确性显然。优化也是简单的,由于 $n,m$ 实在太小所以对于每个 $a_i,b_j$ 一共开 $n+m$ 棵线段树来维护 $\gcd_{j=1}^m\dfrac{a_i}{\gcd(a_i,b_j)}$ 这个式子,而每次删除一个数就相当于在另一边单点修改为 $0$,因为 $\gcd(0,x)=x$ 不影响。然后每次询问全局答案。时间复杂度 $O((n+m)^2\log(n+m)\log V)$。 ```cpp #include<bits/stdc++.h> using namespace std; #define ls p<<1 #define rs p<<1|1 #define Int __int128 const int N=2e3+4; int n,m,T,vis[N][2],A,B; Int a[N],b[N]; Int gcd(Int x,Int y){return y?gcd(y,x%y):x;} Int read(){ Int x=0;char ch=getchar(); while(!isdigit(ch))ch=getchar(); while(isdigit(ch))x=x*10+ch-'0',ch=getchar(); return x; } void write(Int x){ int t[37],_=0; while(x)t[++_]=x%10,x/=10; while(_)putchar(t[_]+'0'),_--; putchar(' ');return; } struct SegmentTree{ Int tr[N<<2]; void pushup(int p){tr[p]=gcd(tr[ls],tr[rs]);} void build(int p,int l,int r){ tr[p]=0;if(l==r)return; int mid=(l+r)>>1; build(ls,l,mid),build(rs,mid+1,r); return; } void update(int p,int l,int r,int ps,Int val){ if(l==r){tr[p]=val;return;} int mid=(l+r)>>1; if(ps<=mid)update(ls,l,mid,ps,val); else update(rs,mid+1,r,ps,val); pushup(p); return; } }t[N][2]; void solve(){ scanf("%d%d",&n,&m),A=n,B=m; for(int i=1;i<=n;i++)a[i]=read(),t[i][0].build(1,1,m),vis[i][0]=0; for(int i=1;i<=m;i++)b[i]=read(),t[i][1].build(1,1,n),vis[i][1]=0; for(int i=1;i<=n;i++)for(int j=1;j<=m;j++)t[i][0].update(1,1,m,j,a[i]/gcd(a[i],b[j])); for(int i=1;i<=m;i++)for(int j=1;j<=n;j++)t[i][1].update(1,1,n,j,b[i]/gcd(b[i],a[j])); while(A>0||B>0){ bool fl=0; for(int i=1;i<=n;i++)if(!vis[i][0]&&t[i][0].tr[1]>1){ for(int j=1;j<=m;j++)t[j][1].update(1,1,n,i,0); A--,vis[i][0]=1,fl=1; } for(int i=1;i<=m;i++)if(!vis[i][1]&&t[i][1].tr[1]>1){ for(int j=1;j<=n;j++)t[j][0].update(1,1,m,i,0); B--,vis[i][1]=1,fl=1; } if(!fl)break; } if(!A||!B){puts("NO");return;} puts("YES"),printf("%d %d\n",A,B); for(int i=1;i<=n;i++)if(!vis[i][0])write(a[i]);puts(""); for(int i=1;i<=m;i++)if(!vis[i][1])write(b[i]);puts(""); return; } int main(){ scanf("%d",&T); while(T--)solve(); return 0; } ``` - UOJ351. 新年的叶子 - 对于一棵树,每次随机染黑一个叶子(可能会重复染黑),期望多少次后直径变小。$n\le 5\times 10^5$。 先把直径拎出来,我们考虑**划分**可能的直径的端点。若直径长度为奇数,那么直径中心为边,那么可能的直径端点被划分成两个集合,否则会被划分为若干个集合。 而直径变小也就是不断染黑知道仅剩一个集合没有被全染黑。先计算出若剩下 $i$ 个叶子没有被染黑,那么一次有效染色的概率就是 $\frac M i$,其中 $M$ 是叶子总数。 设所有集合原大小为 $m$,考虑最后剩下的那个集合,设其集合大小原本为 $sz_i$,结束时剩下 $j$ 个点还是白色,可以得出贡献: $$\frac{1}{m!}A_{sz_i}^{j}[(m-j)!-(m-j-1)!(sz_i-j)] \sum_{k=j+1}^m p_k$$ 含义为:$A_{sz_i}^{j}$ 在 $sz_i$ 中选择 $j$ 个白色的排列方案,$(m-j)!-(m-j-1)!(sz_i-j)$ 则是所有染色排列减去最后一次染色染到当前集合的方案,否则会算重。至于 $\sum_{k=j+1}^m p_k$ 通过前缀和预处理即可。时间复杂度 $O(n)$。 ```cpp #include<bits/stdc++.h> using namespace std; #define int long long const int N=5e5+6; const int mod=998244353; int n,m,ans,rt,len,cnt,sz[N],p[N],fac[N],inv[N],fa[N]; vector<int>g[N]; void init(int n){ fac[0]=inv[0]=inv[1]=1; for(int i=1;i<=n;i++)fac[i]=fac[i-1]*i%mod; for(int i=2;i<=n;i++)inv[i]=inv[mod%i]*(mod-mod/i)%mod; for(int i=1;i<=n;i++)p[i]=(p[i-1]+n*inv[i]%mod)%mod,inv[i]=inv[i-1]*inv[i]%mod; return; } void dfs(int u,int faa,int d){ if(d>len)len=d,rt=u; for(int v:g[u])if(v!=faa)fa[v]=u,dfs(v,u,d+1); return; } void Dfs(int u,int faa,int d,int i){ if(d==len/2)sz[i]++,m++; for(int v:g[u])if(v!=faa)Dfs(v,u,d+1,i); return; } signed main(){ scanf("%lld",&n); for(int i=1;i<n;i++){ int u,v;scanf("%lld%lld",&u,&v); g[u].push_back(v),g[v].push_back(u); } for(int i=1;i<=n;i++)m+=(g[i].size()==1); init(m),m=0,dfs(1,0,0),len=0,dfs(rt,0,0); for(int i=1;i<=len/2;i++)rt=fa[rt]; if(len&1)Dfs(rt,fa[rt],0,++cnt),Dfs(fa[rt],rt,0,++cnt); else for(int v:g[rt])Dfs(v,rt,1,++cnt); for(int i=1;i<=cnt;i++)for(int j=1;j<=sz[i];j++){ int res=(fac[m-j]-fac[m-j-1]*(sz[i]-j)%mod+mod)%mod; res=res*(p[m]-p[j]+mod)%mod,res=res*fac[sz[i]]%mod*inv[sz[i]-j]%mod; ans=(ans+inv[m]*res%mod)%mod; } printf("%lld",ans); return 0; } ``` - CF627D Preorder Test - 给出一棵无根树,每个节点有一个权值。你可以指定树根和访问顺序,使得 dfs 序中前 $k$ 个结点的权值最小值最大,求出这个值。$n\le 2\times 10^5$。 先二分答案 $k$,先随便选择一个点作为根,设 $f_u$ 表示 $u$ 及子树内指定 dfs 序后满足 $a_i\ge k$ 的最长长度。显然对于 $f_v=sz_v$ 的儿子我们可以直接选择,否则我们只能选择剩下的最大值。 统计答案则是 $f_u+se$,表示再选择剩下子树中次大值,因为可以以这个次大值中起始点作为根然后一直访问到 $mx$ 中的那个点即可。子树外的情况不用管因为会被 $u$ 的祖先统计到。 但是这样会 Wrong Answer On Test 25,因为可能子树外全部都是 $a_i\ge k$ 的点,这导致我们计算 $f_u$ 时应该加上 $n-sz_u$。解决方案也很简单,直接找出 $a$ 最小的点设为根即可就能保证子树外的点必然不是全都 $\ge k$。时间复杂度 $O(n\log n)$。 ```cpp #include<bits/stdc++.h> using namespace std; const int N=2e5+6; int n,tot,head[N],f[N],sz[N],a[N],mx,mn,Ans,mid,ans,k,rt; struct edge{int to,nxt;}e[N<<1]; void addedge(int u,int v){ e[++tot].to=v; e[tot].nxt=head[u]; head[u]=tot; return; } void dfs(int u,int fa){ f[u]=1,sz[u]=1; int mx=0,se=0; for(int i=head[u];i;i=e[i].nxt){ int v=e[i].to; if(v==fa)continue; dfs(v,u),sz[u]+=sz[v]; if(f[v]==sz[v])f[u]+=f[v]; else{ if(f[v]>mx)se=mx,mx=f[v]; else if(f[v]>se)se=f[v]; } } f[u]=a[u]<mid?0:f[u]+mx; ans=max(ans,f[u]+se); return; } int main(){ scanf("%d%d",&n,&k),mn=1e9; for(int i=1;i<=n;i++){ scanf("%d",&a[i]),mx=max(mx,a[i]),mn=min(mn,a[i]); if(mn==a[i])rt=i; } for(int i=1;i<n;i++){ int u,v; scanf("%d%d",&u,&v); addedge(u,v),addedge(v,u); } int l=mn,r=mx; while(l<=r){ mid=(l+r)>>1,ans=0; dfs(rt,0); if(ans>=k)Ans=mid,l=mid+1; else r=mid-1; } printf("%d\n",Ans); return 0; } ``` - CF1693F I Might Be Wrong - 给定一个长度为 $n$ 的 `01` 字符串 $S$,你可以进行下列操作任意次:选择 $S$ 的一个连续子串 $S[l,r]$,设 $cnt_0,cnt_1$ 分别表示该子段中字符 `0` 和字符 `1` 的数量,将花费 $|cnt_0-cnt_1|+1$ 的代价对 $S[l,r]$ 进行升序排序。求出使 $S$ 本身升序排序所需的最少代价。 $n\le 2\times 10^5$。 答案上界显然是 $|c_0-c_1|+1$,也就是直接操作 $[1,n]$。那么本质上我们是希望通过多次操作来减少这个代价。 对于每个不同的区间,其代价都是不同的,这是令人烦恼的,我们尝试挖掘一些性质从而刻画 $|c_0-c_1|+1$ 这个代价。考虑这样一个事情:你本可以直接操作 $[1,n]$,但是你会**操作多次**从而使总代价减少,这启发我们同样对于任意一个区间 $[l,r]$ 进入如下的分析:设最终对某个区间 $[l,r]$ 进行了代价为 $d+1$ 的操作,我们能不能通过**操作多次**来减少,或者**更好地刻画**这个代价? (开始前先钦定所有 0 的个数大于 1 的,否则可以反转整个区间然后每个位置取反。) 事实上是可行的,并且也不难证明:对于任意一个代价为 $d+1$ 的区间可以被分割为 $d+1$ 个代价为 $1$ 的区间。首先初始时有 $|c_0-c_1|=d$,那么考虑区间开头的数必然是一个 1(我们的区间不应包括开头的 0),找到一个 1 和 0 数量相等的前缀将其排序,这里便花费了 $|c-c|+1=1$ 的代价,而此时区间开头又变为 0,所以删去开头的若干个 0,我们就递归到了一个总代价为 $d+1-c$ 的区间,那么这一定是不劣于原来的决策的(最劣也就是每次 $c=1$,那么需要 $d+1$ 次操作)。所以我们的决策是**确定的**,每次选取一个 $c_0=c_1$ 的区间进行操作。 后面的就很自然了。我们的目的是将 1 放到数列末尾,那么我们每次肯定是操作一个前缀,否则会有 1 向右移动后又向左移动的操作的产生,这肯定是劣的。而每次选择一个前缀可以保证每个 1 只会向右移动,必定最优。 整理我们的策略:每次找到一个最长的 $c_0=c_1$ 的前缀,然后删去这 $c_0$ 个位于开头的 0。那么这可以通过维护出一个数组 $pos_k$ 表示最大的位置 $i$ 使得 $[1,i]$ 中 $c_0-c_1=k$。那么模拟起来就是先跑出初始时再开头的 $k$ 个 0,然后每次 $k\leftarrow k+(pos_k-k)/2$,意义就是选择 $[k+1,pos_k]$ 这个区间,然后 $k$ 会跳到 $k+(pos_k-k)/2$。时间复杂度 $O(n)$。 ```cpp #include<bits/stdc++.h> using namespace std; const int N=2e5+6; int T,n,cnt,pos[N],now; char s[N]; void solve(){ scanf("%d%s",&n,s+1),now=0,cnt=0; for(int i=2;i<=n;i++)now+=(s[i]<s[i-1]); if(!now){puts("0");return;}pos[0]=0; for(int i=1;i<=n;i++)cnt+=s[i]=='0',pos[i]=0; if(cnt<n-cnt){ cnt=n-cnt; for(int i=1;i<=n/2;i++)swap(s[i],s[n-i+1]); for(int i=1;i<=n;i++)s[i]='0'+'1'-s[i]; }now=0; for(int i=1;i<=n;i++){ now+=s[i]-'0'?-1:1; if(now>=0)pos[now]=i; } int delta=cnt-(n-cnt),k=1,ans=1; while(k<=n&&s[k]=='0')k++;k--; while(k<delta)ans++,k+=(pos[k]-k)/2; printf("%d\n",ans); } int main(){ scanf("%d",&T); while(T--)solve(); return 0; } ``` - [ABC134F] Permutation Oddness - 定义一个排列 $p$ 的权值为 $\sum |i-p_i|$,求权值为 $k$ 的长度为 $n$ 的排列 $p$ 数量。$n\le 50$。 本题核心在于刻画 $|i-p_i|$。绝对值可以想到分讨后拆开,也可以理解为数轴上两个点之间的距离。虽然这两种都不太能刻画,但是 $i-p_i$ 可以看作 $i$ 和 $p_i$ 连边,那么这会形成很多独立的环,这不利于刻画权值。考虑将 $i$ 和 $p_i$ 看作两个不同的事物,比如一个表示小球一个表示盒子,而小球在左边拍成一列,盒子在右边排成一列,这就是一个二分图。那么 $|i-p_i|$ 就容易想到:$i\to p_i$ 连边后与水平线的交点个数。水平线定义为两个相邻小球之间连向对应的两个相邻盒子之间。 那么这是一种比较自然的刻画方式。得出这个模型后 dp 不难。设 $f_{i,j,k}$ 表示只考虑 $1\sim i$ 的小球与盒子,有 $j$ 个小球和盒子没有被选择,当前权值和为 $k$,即所有已连线与水平线的交点。转移分类当前这一层进行几次配对 ($0/1/2$)即可,是 trival 的,不做赘述。时间复杂度 $O(n^4)$。 ```cpp #include<bits/stdc++.h> using namespace std; #define int long long const int N=52; const int M=N*N; const int mod=1e9+7; int n,K,dp[N][N][M]; signed main(){ scanf("%lld%lld",&n,&K),dp[0][0][0]=1; for(int i=1;i<=n;i++)for(int j=0;j<=i;j++)for(int k=2*j;k<=K;k+=2){ dp[i][j][k]=dp[i-1][j][k-2*j]*(2*j+1)%mod; dp[i][j][k]=(dp[i][j][k]+dp[i-1][j+1][k-2*j]*(j+1)%mod*(j+1)%mod)%mod; if(j)dp[i][j][k]=(dp[i][j][k]+dp[i-1][j-1][k-2*j])%mod; } printf("%lld\n",dp[n][0][K]); return 0; } ``` - [AGC047F] Rooks - 有 $n$ 个车在一张无限大的棋盘上,第 $i$ 个在 $(x_i,y_i)$。每行每列最多一个车。有一个卒,会替换第 $i$ 个车,可以走八连通,但是不能走到被车攻击的地方。吃车的时候可以走对角,否则只能走上下左右。对于 $i=1\dots n$,求出吃掉最多的车时的最小步数。$n\le 10^6$。 先将 $x_i$ 排序后将 $x,y$ 离散化。那么注意到这个卒吃掉的车的 $x,y$ 必定是一个完整的区间。所以容易想到区间 dp,$f_{l,r,0/1}$ 表示当前吃掉了 $[l,r]$ 内的车并且在 $l/r$ 上。对于转移则算出 $mn=\min_{i\in [l,r]}y_i,mx=\max_{i\in[l,r]}y_i$,那么弱向左扩展一格便判断 $y_{l-1}$ 是否为 $mn-1$ 或者 $mx+1$,向右同理。这样是 $O(n^3)$ 的,因为要枚举起点 $i$ 来初始化 $f_{i,i,0/1}=0$。那么这样会有很多重复状态,因为对于一个固定的区间 $[l,r]$ 一定存在一个极大的可以到达的区间 $[L,R]$,不管按什么顺序操作最后都能到这个极大的区间。所以考虑状态定义反过来为除了 $[l,r]$ 都吃掉了的答案。那这就是 $O(n^2)$ 的了。 注意到如果区间内满足 $|y_i-y_{i+1}|=1$,也就是区间为一段 $x,y$ 的连续段,那么此时我们删除的策略是固定的:删除 $l$ 后就一定会删除 $l+1,\dots r$。那么就可以将这个区间看作一个点,然后每次扩展到它的下一个极大区间 $[L,R]$。对于这个做法状态数只有 $O(n)$ ,直接记忆化搜索即可。 如何证明?首先先将整体看作若干段 $|y_i=y_{i+1}|=1$ 的连续段,呈现在平面上也就是若干个正方形相互包含,那么我们的状态数也就输层数级别的,即相互包含的数量。而对于两个区间的合并只会有一段连续段被两个正方形所共同囊括,所以每个正方形最多只会被包含两次,所以状态数 $O(n)$。使用 map 记忆化搜索 $O(n\log n)$。 ```cpp #include<bits/stdc++.h> using namespace std; #define ll long long const int N=2e5+6; const ll inf=1e18; int n,L[N],R[N];ll ans[N]; vector<int>lsh; map<int,ll>dp[2][N]; struct node{ int x,y,i; bool operator<(const node&rhs)const{return x<rhs.x;} }a[N]; int dis(node a,node b){return abs(a.x-b.x)+abs(lsh[a.y]-lsh[b.y]);} ll dfs(int l,int r,int mn,int mx,int pos){ if(dp[pos][l][r])return dp[pos][l][r]; else dp[pos][l][r]=inf; node now=pos?a[r]:a[l]; if(l>1&&(a[l-1].y==mn-1||a[l-1].y==mx+1)){ int k=L[l-1]; dp[pos][l][r]=min(dp[pos][l][r],dfs(k,r,min(mn,a[k].y),max(mx,a[k].y),0)+dis(now,a[k])); } if(r<n&&(a[r+1].y==mn-1||a[r+1].y==mx+1)){ int k=R[r+1]; dp[pos][l][r]=min(dp[pos][l][r],dfs(l,k,min(mn,a[k].y),max(mx,a[k].y),1)+dis(now,a[k])); } if(dp[pos][l][r]==inf)dp[pos][l][r]=-(r-l); return dp[pos][l][r]; } int main(){ scanf("%d",&n); for(int i=1;i<=n;i++)scanf("%d%d",&a[i].x,&a[i].y),a[i].i=i,lsh.push_back(a[i].y); sort(lsh.begin(),lsh.end()); for(int i=1;i<=n;i++)a[i].y=lower_bound(lsh.begin(),lsh.end(),a[i].y)-lsh.begin(); sort(a+1,a+n+1); L[1]=1,R[n]=n; for(int i=2;i<=n;i++)L[i]=abs(a[i].y-a[i-1].y)==1?L[i-1]:i; for(int i=n-1;i;i--)R[i]=abs(a[i].y-a[i+1].y)==1?R[i+1]:i; for(int i=1;i<=n;i++)ans[a[i].i]=dfs(i,i,a[i].y,a[i].y,0); for(int i=1;i<=n;i++)printf("%lld\n",ans[i]); return 0; } ``` - [ARC146E] Simple Speed - 给定长为 $n$ 的序列 $a_i$ ,求满足要求的序列 $b$ 的数量:元素为 $[1, n]$ 且 $i$ 刚好出现 $a_i$ 次,且 $|b_i - b_{i - 1}| = 1$。$n,a_i\le 2\times 10^5$。 ~~有个笨蛋把题目看成 $|b_i-b_{i-1}|\le 1$ 了。~~ 按照值域加入数,从小往大放,考虑加入完所有 $i$ 后加入 $i+1$。首先 $i+1$ 必须囊括所有相邻 $i$ 之间的空位否则序列非法。而 $i+1$ 也只能放在相邻 $i$ 的中或者边界上的 $i$ 的旁边。呼之欲出一个简单的 dp:$f_{i,j,0/1,0/1}$ 表示当前加入到了 $i$,有 $j$ 对相邻的 $i$,序列最左边/最右边是不是 $i$。 瓶颈在于 $j$ 的状态,而显然 $j$ 每次的变化量范围很小。如果不考虑两边放置 $i$ 的情况,那么每次的 $j$ 都是确定的。所以两边放/不妨只会造成 $1$ 的影响,$j$ 的变化量大约 $\le 5$,时间复杂度 $O(n)$。 ```cpp #include<bits/stdc++.h> using namespace std; #define int long long const int N=2e5+6; const int mod=998244353; int n,a[N],num[N],ans,dp[N][5][2][2],fac[N<<1],ifac[N<<1]; int qpow(int a,int b){ int res=1; while(b){ if(b&1)res=res*a%mod; a=a*a%mod; b>>=1; } return res; } void init(int n){ fac[0]=ifac[0]=1; for(int i=1;i<=n;i++)fac[i]=fac[i-1]*i%mod; ifac[n]=qpow(fac[n],mod-2); for(int i=n-1;i;i--)ifac[i]=ifac[i+1]*(i+1)%mod; return; } int C(int n,int m){ if(n<0||m<0||n<m)return 0; return fac[n]*ifac[m]%mod*ifac[n-m]%mod; } inline void add(int&x,int y){x=(x+y>=mod?x+y-mod:x+y);} signed main(){ scanf("%lld",&n),init(N*2-2); for(int i=1;i<=n;i++)scanf("%lld",&a[i]); num[1]=a[1]-1,dp[1][2][1][1]=1; for(int i=2;i<=n;i++){ num[i]=a[i]-num[i-1]; for(int j=0;j<5;j++)for(int l=0;l<2;l++)for(int r=0;r<2;r++)if(dp[i-1][j][l][r]){ int c=num[i-1]+j-2; for(int L=0;L<=l;L++)for(int R=0;R<=r;R++){ if(L&&R)add(dp[i][2-j][L][R],dp[i-1][j][l][r]*C(a[i]-1,c+1)%mod); else if(L||R)add(dp[i][3-j][L][R],dp[i-1][j][l][r]*C(a[i]-1,c)%mod); else add(dp[i][4-j][L][R],dp[i-1][j][l][r]*C(a[i]-1,c-1)%mod); } } } for(int j=0;j<5;j++)if(num[n]+j-2==0)for(int l=0;l<2;l++)for(int r=0;r<2;r++)add(ans,dp[n][j][l][r]); printf("%lld",ans); return 0; } ``` - P4845 LJJ 爱数树 - 给定一棵树,点有点权。要求放置至多 $k$ 个摄像头,每个摄像头可以检测与其距离 $\le r$ 的所有点,使得所有被检测到的点的权值和最大。$n,k,r\le 10^3$。 截止至 2023/12/14 是最优解 439ms /cengyiceng。 每个点只有有摄像头、被摄像头检测到、不被检测到三种情况。 一个显然的树形 dp 是 $f_{u,i,j,k}$ 表示以 $u$ 为根的子树内放了 $i$ 个摄像头,$j$ 表示距离 $u$ 最远的**等待点**与 $u$ 的距离,$k$ 表示距离 $u$ 最近的摄像头与其距离。 之所以定义 $j$ 是因为我们钦定每个点只有有摄像头、被摄像头检测到、不被检测到三种情况。而从子树去 dp 时注意有的点会被钦定为被摄像头检测到,然而此时子树内的摄像头并不能检测到它,也就是检测到它的摄像头在子树外面,我们称这样的点为**等待点**。 那么一个极其常见的套路就是 $j,k$ 可以被合并成一维。~~我记得我做过完全一样套路的题目,不知道是不是忘记写在这里了。~~ 首先 $j+k>r$,否则此时子树内的等待点就可以直接被子树内最浅的这个摄像头照到。设该等待点被子树外一点 $w$ 上的摄像头检测,那么 $dis(u,w)+j\le r$,可以得到 $k>dis(u,w)$,那么 $w$ 上的摄像头可以检测到子树 $u$ 内所有摄像头能检测到的子树外的点,注意对于子树内我只关心 $j$,所以此时 $u$ 子树内的等在未来不会用任何作用,$k$ 就可以不用记录。而若不存在等待点就只记录 $k$ 就好了。 所以我们转化为两个 dp:$f_{u,i,j},g_{u,i,j}$ 表示以 $u$ 为根的子树内放置了 $i$ 个摄像头,$f$ 中的 $j$ 表示最浅的摄像头与 $u$ 的距离,$g$ 中 $j$ 表示最深的等待点与 $u$ 的距离。 对于 dp 转移考虑树形背包的形式,先将 $f_u,f_v$ 和 $g_u,g_v$ 分别合并,然后考虑 $f_u+g_v$ 和 $f_v+g_u$ 的情况,此时判断二者之间的距离,也就是 $j+k+1$ 和 $r$ 的大小关系即可确定转移到新的 $f$ 或 $g$。复杂度 $O(nkr)$。 再次优化,发现当 $kr\ge n$ 时必定可以覆盖所有点,所以答案就是 $\sum a_i$。证明即考虑此时以某个点为根时的树高。若树高 $\le r$ 则在根放置摄像头即可,否则选择最深点的 $r$ 级祖先放置摄像头(没有就放在根上),每次放置必定至少检测 $r$ 个点,故证毕。 所以此时 $kr\le n$,时间复杂度 $O(nkr)<O(n^2)$。 ```cpp #include<bits/stdc++.h> using namespace std; const int N=1e3+4; const int NN=N<<1; const int inf=1e9; int T,n,k,r,tot,head[N],f[N][NN],g[N][NN],F[NN],G[NN],ans,a[N],sz[N]; struct edge{int to,nxt;}e[N<<1]; void addedge(int u,int v){ e[++tot].to=v; e[tot].nxt=head[u]; head[u]=tot; return; } int id(int j,int k){return j*(r+2)+k;} void Max(int&x,int y){x=x<y?y:x;} void dfs(int u,int fa){ sz[u]=1; for(int i=0;i<=k;i++)for(int j=0;j<=r+1;j++)f[u][id(i,j)]=g[u][id(i,j)]=-inf; f[u][id(0,r+1)]=0,f[u][id(1,0)]=g[u][id(0,0)]=a[u]; for(int i=head[u];i;i=e[i].nxt){ int v=e[i].to; if(v==fa)continue; dfs(v,u); for(int i=0;i<NN;i++)F[i]=G[i]=-inf; for(int j1=min(sz[u],k);j1>=0;j1--)for(int k1=0;k1<=r+1;k1++)if(f[u][id(j1,k1)]>=0||g[u][id(j1,k1)]>=0) for(int j2=min(sz[v],k-j1);j2>=0;j2--)for(int k2=0;k2<=r+1;k2++)if(f[v][id(j2,k2)]>=0||g[v][id(j2,k2)]>=0){ int fu=f[u][id(j1,k1)],gu=g[u][id(j1,k1)],fv=f[v][id(j2,k2)],gv=g[v][id(j2,k2)]; Max(F[id(j1+j2,min(k1,k2+1))],fu+fv),Max(G[id(j1+j2,max(k1,min(r+1,k2+1)))],gu+gv); if(k2+1+k1<=r)Max(F[id(j1+j2,k1)],fu+gv),Max(F[id(j1+j2,min(r+1,k2+1))],fv+gu); else Max(G[id(j1+j2,min(r+1,k2+1))],fu+gv),Max(G[id(j1+j2,k1)],fv+gu); } sz[u]+=sz[v],memcpy(f[u],F,sizeof(f[u])),memcpy(g[u],G,sizeof(g[u])); } return; } void solve(){ scanf("%d%d%d",&n,&k,&r),tot=ans=0; for(int i=1;i<=n;i++)scanf("%d",&a[i]),head[i]=0; for(int i=1;i<n;i++){ int u,v; scanf("%d%d",&u,&v); addedge(u,v),addedge(v,u); } if(k*r>=n){ for(int i=1;i<=n;i++)ans+=a[i]; printf("%d\n",ans); return; } dfs(1,0); for(int i=0;i<=r+1;i++)ans=max(ans,f[1][id(k,i)]); printf("%d\n",ans); return; } int main(){ scanf("%d",&T); while(T--)solve(); return 0; } ```