做题记录 2026.1.10~2026.1.31

怎么老是忘了差分和正难则反

P11834 [省选联考 2025] 岁月

B 性质

原图最小生成树唯一,非树边直接扔掉,对于树边,把双向边连接的两个点缩在一起。假设有 \(k\) 条双向边,则缩点后有 \(n-k\) 个点,对应 \(n-k\) 个单向边定向方案。总方案数

\[\frac{1}{4^{n-1}}\sum_{k=0}^{n-1}\binom{n-1}{k-1}(n-k) \]

C 性质

题意限制等价于缩点后的图只有一个零入度点。考虑类似主旋律一样,设 \(f_S\) 表示 \(S\) 强连通概率,\(g_{S,i}\) 表示 \(S\) 为 \(i\) 个独立强连通块概率,\(g_S=\sum\limits_i(-1)^{i+1}g_{S,i}\)。

\[\begin{align*} f_S&=1-\sum_{i=1}^n\sum_{j=1}^n\binom{j}{i}(-1)^{j-i}\sum_{T\subseteq S}g_{T,j}\frac{1}{2^{E_{T,S/T}}} \\ &=1-\sum_{T\subseteq S}g_T\frac{1}{2^{E_{T,S/T}}} \end{align*} \]

\[g_S=-\sum_{\substack{T\subsetneq S \\ \min T=\min S}}f_Tg_{S/T}\frac{1}{4^{E_{T,S/T}}}+f_S\]

\[\begin{align*} ans&=\sum_{i=1}^n\binom{i}{1}(-1)^{i-1}\sum_{T\subseteq S}g_{T,i}\frac{1}{2^{E_{T,S/T}}} \\ &=\sum_{T\subseteq S}\frac{1}{2^{E_{T,S/T}}}\sum_{i=1}^n(-1)^{i-1}ig_{T,i} \\ &=\sum_{T\subseteq S}\frac{1}{2^{E_{T,S/T}}}h_T \end{align*} \]

为了计算答案,我们需要定义 \(h_S=\sum_{i=1}^n(-1)^{i-1}ig_{S,i}\)。

\[h_S=-\sum_{\substack{T\subsetneq S \\ \min T=\min S}}f_T(g_{S/T}+h_{S/T})\frac{1}{4^{E_{T,S/T}}}+f_S\]

正解 *

考虑 kruskal 过程,按边权分层,注意到要满足题意需要 \(G'\) 每一层做完后连通性与 \(G\) 做完该层后连通性相同。

每一个连通块有一些点可以作为最小外向生成树的根,称为合法根。如果把每一个连通块缩成一个点,则变为 C 性质。枚举合法根,把双向边拆成两个单向边,只保留指向合法根的边,再缩弱连通块,直接跑 C 性质。求答案略比 C 性质复杂。

本节下文符号均定义在新图(缩点后的图)上。设 \(S\) 为合法根点集,\(U\) 为总点集,\(E_{S,T}\) 为 \(S\) 到 \(T\) 的有向边数。

\[\begin{align*} ans_S&=\frac{1}{2^{E_{U/S,S}}}f_S+\sum_{i\ge 1}(-1)^i\sum_{T\subseteq U/S}f_Sg_{T,i}\frac{1}{2^{E_{S,T}+E_{T,S}+E_{U/(S+T),S+T}}} \\ &=\frac{1}{2^{E_{U/S,S}}}f_S-\sum_{T\subseteq U/S}f_Sg_T\frac{1}{2^{E_{S,T}+E_{T,S}+E_{U/(S+T),S+T}}} \end{align*} \]

时间复杂度 *

考虑 kruskal 重构树,做一次 \(C\) 性质相当于转移一个树上结点。设 \(a_u\) 表示 \(u\) 子树叶子数量,\(ch_u\) 表示 \(u\) 的儿子。则有计算式

\[T(a_u)=\sum_{v\in ch_u}T(a_v)+3^{|ch_u|}\prod_{v\in ch_u}(2^{a_v}-1) \]

总复杂度 \(T(n)\)。

考虑放缩,在 \(x\) 为正整数时,\(2^x-1\le 3^{x-1}\),故

\[T(a_u)=\sum_{v\in ch_u}T(a_v)+3^{a_u} \]

考虑归纳证明 \(T(n)=\mathcal{O}(3^n)\)。

对于两个正整数 \(a\le b\) 且 \(a+b=x\),若

  • \(a=1,b=x-1\),\(3^a+3^b=3+3^{x-1}=\mathcal{O}(3^{x-1})\)
  • \(b\le x-2\),\(3^a+3^b\le 2\times 3^{x-2}=\mathcal{O}(3^{x-1})\)

累加,可得

\[T(a_u)=\mathcal{O}(3^{a_u-1})+3^{a_u}=\mathcal{O}(3^{a_u}) \]

总复杂度 \(T(n)=\mathcal{O}(3^n)\)

ARC092F Two Faced Edges

差不多是自己做出来的,中间和题解对了一下。

不难想到题意可转化为 same 当且仅当 \(u\) 与 \(v\) 之间不经过 \((u,v)\) 互相可达或互相不可达。

判断 \(v\) 不经过 \((u,v)\) 可达 \(u\) 是好判的,搜一下即可。

判断 \(u\) 不经过 \((u,v)\) 可达 \(v\),则考虑以 \(u\) 为根的 dfs 生成树,可达 \(v\) 相当于 \(v\) 不是 \(u\) 的儿子或者后面的兄弟可达 \(v\)。由此,考虑正反各做一遍,按顺序枚举 \(u\) 的出度,从出度开始 dfs,考虑在不回到 \(u\) 的情况下能到哪些点。如果还没搜 \(v\) 时 \(v\) 已被标记,则 \(u\) 不经过 \((u,v)\) 可达 \(v\)。这相当于找一个 \(u\) 到 \(v\) 的简单路径,上述判定的充要性显然。

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
typedef pair<int,int> pii;
typedef pair<int,ll> pil;
typedef pair<ll,int> pli;
typedef pair<ll,ll> pll;
template<typename T>
void chkmin(T &x,const T &y){x=min(x,y);}
template<typename T>
void chkmax(T &x,const T &y){x=max(x,y);}
const int N=1005,M=200005;
int n,m,vis[N],cnt[N],ans[M],f[N][N];
vector<pii> G[N];
vector<int> vec;
void dfs(int u){
    if(vis[u]) return;
    vis[u]=1,cnt[u]++,vec.push_back(u);
    for(auto p:G[u]) dfs(p.first);
}
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,i});
    }
    for(int i=1;i<=n;i++){
        for(int j=1;j<=n;j++) vis[j]=cnt[j]=0;
        vis[i]=1;
        for(auto p:G[i]){
            if(vis[p.first]) ans[p.second]=1;
            dfs(p.first);
            vec.clear();
        }
        for(int j=1;j<=n;j++) vis[j]=cnt[j]=0;
        vis[i]=1;
        reverse(G[i].begin(),G[i].end());
        for(auto p:G[i]){
            if(vis[p.first]) ans[p.second]=1;
            dfs(p.first);
            vec.clear();
        }
        for(int j=1;j<=n;j++) f[i][j]=vis[j];
    }
    for(int i=1;i<=n;i++) for(auto p:G[i]) ans[p.second]^=f[p.first][i];
    for(int i=1;i<=m;i++){
        if(ans[i]) printf("diff\n");
        else printf("same\n");
    }
    return 0;
}

CF875F Royal Questions

有一个自然的想法是转为匹配,可惜不好做。考虑为什么每一个公主均指向两个王子,这启示我们将这两个王子连边。经过一些思考,发现问题变为要给一些边定向,使得每个点最多有一个出度,最大化被定向的边边权和。注意到每个点最多有一个出度等价于内向基环森林,问题变为求最大生成基环森林。

考虑魔改 kruskal,对每一个连通块,维护 \(f_0\) 表示正常的最大生成树,\(f_1\) 表示最大生成基环森林(注意是森林,即可能有多个环,但是要求不能再加边),乱斗一下转移即可,证明可以利用不优性调整。

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
typedef pair<int,int> pii;
typedef pair<int,ll> pil;
typedef pair<ll,int> pli;
typedef pair<ll,ll> pll;
template<typename T>
void chkmin(T &x,const T &y){x=min(x,y);}
template<typename T>
void chkmax(T &x,const T &y){x=max(x,y);}
const int inf=0x7f3f3f3f;
const int N=200005;
int n,m,fa[N],f[N][2];
struct Edge{
    int u,v,w;
};
Edge es[N];
bool cmp(Edge x,Edge y){
    return x.w>y.w;
}
int find(int x){
    if(fa[x]==x) return x;
    return fa[x]=find(fa[x]);
}
int main(){
    scanf("%d%d",&n,&m);
    for(int i=1;i<=m;i++) scanf("%d%d%d",&es[i].u,&es[i].v,&es[i].w);
    sort(es+1,es+m+1,cmp);
    for(int i=1;i<=n;i++) fa[i]=i;
    for(int i=1;i<=m;i++){
        int u=find(es[i].u),v=find(es[i].v),w=es[i].w;
        if(u==v) chkmax(f[u][1],f[u][0]+w);
        else{
            fa[u]=v;
            f[v][1]=max(f[u][1]+f[v][1],max(f[u][0]+f[v][1],f[u][1]+f[v][0])+w);
            f[v][0]+=f[u][0]+w;
        }
    }
    int ans=0;
    for(int i=1;i<=n;i++) if(fa[i]==i) ans+=max(f[i][0],f[i][1]);
    printf("%d\n",ans);
    return 0;
}

P7124 [Ynoi2008] stcm

同类题

考虑对 dfn 分治,每次处理 \(l\le dfn_u\le mid<dfn_u+sz_u-1\le r\) 的 \(u\),注意到这样的区间相互包含,使用双指针扫一遍即可。复杂度 \(\mathcal{O}(n\log n)\)

重要结论:如果只支持 \(\mathcal{O}(T)\) 加/删点,则维护所有子树补信息复杂度为 \(\mathcal{O}(Tn\log n)\)。

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
typedef pair<int,int> pii;
template<typename T>
void chkmin(T &x,const T &y){x=min(x,y);}
template<typename T>
void chkmax(T &x,const T &y){x=max(x,y);}
const int inf=0x3f3f3f3f;
const ll infll=0x3f3f3f3f3f3f3f3f;
const int N=100005;
void pc(char c){
    putchar_unlocked(c);
}
void write(int x){
    if(x>=0&&x<10){
        pc('0'+x);
        return;
    }
    write(x/10);
    pc('0'+x%10);
}
int n,fa[N],dfn[N],tot,sz[N],id[N];
vector<int> G[N];
void dfs(int u){
    id[dfn[u]=++tot]=u,sz[u]=1;
    for(auto v:G[u]){
        dfs(v);
        sz[u]+=sz[v];
    }
}
template<typename T>
void clear(T &v){
    T().swap(v);
}
void solve(int l,int r,vector<int> &vec){
    if(l==r){
        for(auto i:vec) pc('='),write(i);
        return;
    }
    int mid=(l+r)>>1;
    vector<int> L,R,M;
    for(auto i:vec){
        if(dfn[i]+sz[i]-1<=mid) L.push_back(i);
        else if(dfn[i]>mid) R.push_back(i);
        else M.push_back(i);
    }
    clear(vec);
    sort(M.begin(),M.end(),[&](int x,int y){return dfn[x]<dfn[y];});
    int i=l,j=r;
    for(auto k:M){
        while(i<dfn[k]) pc('+'),write(id[i++]);
        while(j>dfn[k]+sz[k]-1) pc('+'),write(id[j--]);
        pc('='),write(k);
    }
    for(;i>l;i--) pc('-');
    for(;j<r;j++) pc('-');
    clear(M);
    if(L.size()){
        for(int i=r;i>mid;i--) pc('+'),write(id[i]);
        solve(l,mid,L);
        for(int i=r;i>mid;i--) pc('-');
    }
    if(R.size()){
        for(int i=l;i<=mid;i++) pc('+'),write(id[i]);
        solve(mid+1,r,R);
        for(int i=l;i<=mid;i++) pc('-');
    }
}
void __CLEAR__(){
    for(int i=1;i<=n;i++) G[i].clear();
    tot=0;
}
bool __SOLVE__(){
    if(scanf("%d",&n)==EOF) return false;
    __CLEAR__();
    for(int i=2;i<=n;i++){
        scanf("%d",&fa[i]);
        G[fa[i]].push_back(i);
    }
    dfs(1);
    vector<int> qs;
    for(int i=1;i<=n;i++) qs.push_back(i);
    solve(1,n,qs);
    pc('!'),pc('\n');
    return true;
}
int main(){
    int T=1;
    while(true) if(!__SOLVE__()) break;
    return 0;
}

P5311 [Ynoi2011] 成都七中

好难的题,第一步就没想到……

树上连通性问题,考虑点分治。对每个连通块,找到最深的包含它的分治子树(存在性证明:从根开始,不断往下走,直到走不动),设此时分治中心为 \(r\),不难发现 \(r\) 在此连通块中。

考虑把所有在 \(r\) 处的询问一起处理,注意到一个点 \(u\) 在询问 \([l,r]\) 中与 \(r,x\) 在同一连通块里当且仅当 \(l\le\min\{path_{u,r}\}\le\max\{path_{u,r}\}\le r\)。这是一个二维偏序的限制,扫描线即可,下文以扫 \(r\) 为例。

接下来考虑数颜色,我们要想办法在某种意义上让同色点只贡献一次。注意到查询是一个后缀,所以如果查询区间中有某种颜色,则该颜色点中的最后一个必然被查询覆盖,所以只让每种颜色 \(l,\min\) 一维最大的点做贡献即可。

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
typedef pair<int,int> pii;
typedef pair<int,ll> pil;
typedef pair<ll,int> pli;
typedef pair<ll,ll> pll;
template<typename T>
void chkmin(T &x,const T &y){x=min(x,y);}
template<typename T>
void chkmax(T &x,const T &y){x=max(x,y);}
const int inf=0x3f3f3f3f;
const ll infll=0x3f3f3f3f3f3f3f3f;
const int N=100005;
int n,m,c[N],ans[N],vis[N],sz[N],id[N],mn[N],mx[N];
vector<int> G[N],vec[N];
struct Query{
    int l,r,x;
};
Query qs[N];
template<typename T>
void clear(T &x){
    T().swap(x);
}
pii calcRt(int u,int f,int n){
    sz[u]=1;
    int mx=0;
    pii mn{inf,0};
    for(auto v:G[u]){
        if(vis[v]||v==f) continue;
        chkmin(mn,calcRt(v,u,n));
        sz[u]+=sz[v],chkmax(mx,sz[v]);
    }
    chkmin(mn,{max(mx,n-sz[u]),u});
    return mn;
}
struct Point{
    int x,y,type,val;// type=0 add,type=1 query
};
vector<Point> pts;
void dfs(int u,int f){
    sz[u]=1;
    if(u==f) mn[u]=mx[u]=u;
    else mn[u]=min(u,mn[f]),mx[u]=max(u,mx[f]);
    pts.push_back({mx[u],mn[u],0,c[u]});
    for(auto v:G[u]){
        if(vis[v]||v==f) continue;
        if(u==f) id[v]=v;
        else id[v]=id[u];
        dfs(v,u);
        sz[u]+=sz[v];
    }
}
bool cmd(Point x,Point y){
    if(x.x!=y.x) return x.x<y.x;
    return x.type<y.type;
}
vector<int> colors;
int lst[N],tr[N];
#define lowbit(x) (x&(-x))
void add(int x,int v){
    for(;x<=n;x+=lowbit(x)) tr[x]+=v;
}
int query(int x){
    int sum=0;
    for(;x;x-=lowbit(x)) sum+=tr[x];
    return sum;
}
void solve(int r,int cnt){
    vector<int> qvec=vec[r];
    clear(vec[r]);
    r=calcRt(r,r,cnt).second;
    // printf("rt %d\n",r);
    pts.clear();
    dfs(r,r);
    vis[r]=1;
    for(auto i:qvec){
        if(mn[qs[i].x]<qs[i].l||mx[qs[i].x]>qs[i].r) vec[id[qs[i].x]].push_back(i);
        else pts.push_back({qs[i].r,qs[i].l,1,i});
    }
    sort(pts.begin(),pts.end(),cmd);
    // for(auto p:pts) printf("(%d,%d,%d,%d)\n",p.x,p.y,p.type,p.val);
    for(auto p:pts){
        if(!p.type){
            if(!lst[p.val]) colors.push_back(p.val);
            if(lst[p.val]<p.y){
                if(lst[p.val]) add(lst[p.val],-1);
                lst[p.val]=p.y;
                add(p.y,1);
            }
        }
        else ans[p.val]=query(n)-query(p.y-1);
    }
    for(auto c:colors){
        add(lst[c],-1);
        lst[c]=0;
    }
    colors.clear();
    for(auto v:G[r]){
        if(vis[v]) continue;
        solve(v,sz[v]);
    }
}
int main(){
    scanf("%d%d",&n,&m);
    for(int i=1;i<=n;i++) scanf("%d",&c[i]);
    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);
    }
    for(int i=1;i<=m;i++) scanf("%d%d%d",&qs[i].l,&qs[i].r,&qs[i].x);
    for(int i=1;i<=m;i++) vec[1].push_back(i);
    solve(1,n);
    for(int i=1;i<=m;i++) printf("%d\n",ans[i]);
    return 0;
}

P5067 [Ynoi Easy Round 2014] 长存不灭的过去、逐渐消逝的未来

随便口胡的,大概是对的,感觉评黑是因为实现难度(?

考虑建线段树,把每一个区间的表达式进行缩减,变为 ...)...)...(...(...,每个 ... 都可以用 \(\mathcal{O}(1)\) 个运算符表达,合并的时候直接暴力合并,因为保证了任意前缀括号层数为 \(50\),故区间内括号层数也为 \(50\)(\(l\) 前面最多剩下 \(50\) 个左括号,故区间里面最多剩下 \(50\) 个右括号和 \(50\) 个左括号),所以复杂度 \(\mathcal{O}(50n\log n)\)。

P4117 [Ynoi2018] 五彩斑斓的世界

神题。

想到了要以某种方式均摊,但不会。

考虑把修改分类,设 \(k\) 表示最大值。

若 \(2x\ge k\),则 \(k-x\le x\),则 \(k'\le x\),直接枚举 \(x+1\sim k\) 的数值做修改。

若 \(2x<k\),则枚举 \(0\sim x\) 的数值把它们 \(+x\),再打全局减标记(这里有一个细节是 \(0\) 要特殊处理,否则后面用并查集会导致 \(0\rightarrow 2x\))。

这样做后,\(k\) 单调不增,且枚举量与 \(\Delta k\) 同阶,均摊 \(\mathcal{O}(V)\)。

接下来考虑分块,用并查集维护修改、查询即可。对于整块修改/查询,直接合并/查询并查集,由于结构特殊,这里不需要 \(find\);对于散块修改/查询,暴力重构/扫描(正是这里需要用并查集,否则可以直接记 \(cnt\))。时间复杂度 \(\mathcal{O}(n\sqrt{n})\)。

由于卡空间,需要把询问离线,对每个块独立处理。

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
typedef pair<int,int> pii;
typedef pair<int,ll> pil;
typedef pair<ll,int> pli;
typedef pair<ll,ll> pll;
template<typename T>
void chkmin(T &x,const T &y){x=min(x,y);}
template<typename T>
void chkmax(T &x,const T &y){x=max(x,y);}
const int inf=0x3f3f3f3f;
const ll infll=0x3f3f3f3f3f3f3f3f;
const int N=1000005,B=999,M=500005,V=100005;
// const int N=15,B=3,M=500005,V=100005;
inline int read(){
    int x=0,t=1;
    char c;
    while(c=getchar_unlocked()){
        if(c=='-') t=-1;
        else if(c>='0'&&c<='9') break;
    }
    do{
        if(c<'0'||c>'9') break;
        x=x*10+c-'0';
    }
    while(c=getchar_unlocked());
    return x*t;
}
int n,m,a[N],ans[M];
int op[M],l[M],r[M],X[M];
int L[N/B+5],R[N/B+5],blkId[N];
int fa[V],O,cnt[V];
void merge(int a,int b){
    fa[a]=b,cnt[b]+=cnt[a];
}
inline int find(int x) {
    if(fa[x]==fa[fa[x]]) return fa[x];
    while(x^fa[x]) x=fa[x]=fa[fa[x]]=fa[fa[fa[x]]];
    return x;
}
int main(){
    n=read(),m=read();
    for(int i=1;i<=n;i++) a[i]=read();
    for(int i=1;i<=m;i++) op[i]=read(),l[i]=read(),r[i]=read(),X[i]=read();
    for(int i=1;;i++){
        L[i]=R[i-1]+1,R[i]=min(R[i-1]+B,n);
        if(L[i]>n) break;
        for(int j=L[i];j<=R[i];j++) blkId[j]=i;
    }
    for(int i=1;i<=blkId[n];i++){
        int mx=0;
        for(int j=0;j<V;j++) fa[j]=j,cnt[j]=0;
        for(int j=L[i];j<=R[i];j++) chkmax(mx,a[j]),cnt[a[j]]++;
        O=0;
        for(int id=1;id<=m;id++){
            int nl=blkId[l[id]],nr=blkId[r[id]],x=X[id];
            // type: 1-full modify 2-full query 3-part modify 4-part query
            int type=0,ql=0,qr=0;
            if(nl==i||nr==i) type=2+op[id],ql=max(l[id],L[i]),qr=min(r[id],R[i]);
            else if(nl<i&&i<nr) type=op[id];
            else continue;
            if(type==1||type==2){
                if(type==1){
                    if(mx-O>x&&(x<<1)>=mx-O){
                        for(int i=x+O+1;i<=mx;i++) if(fa[i]==i) merge(i,i-x);
                        mx=max(mx-x,x+O);
                    }
                    else if(mx-O>x){
                        for(int i=O+1;i<=x+O;i++) if(fa[i]==i) merge(i,i+x);
                        O+=x;
                    }
                }
                else if(!x) ans[id]+=cnt[0];
                else if(x+O<=mx&&fa[x+O]==x+O) ans[id]+=cnt[x+O];
            }
            else{
                for(int j=L[i];j<=R[i];j++) a[j]=find(a[j]);
                if(type==3){
                    for(int j=ql;j<=qr;j++) if(a[j]-O>x){
                        cnt[a[j]]--;
                        a[j]-=x;
                        cnt[a[j]]++;
                    }
                    mx=0;
                    for(int j=L[i];j<=R[i];j++) chkmax(mx,a[j]);
                }
                else if(x) for(int j=ql;j<=qr;j++) ans[id]+=(a[j]-O==x);
                else for(int j=ql;j<=qr;j++) ans[id]+=(!a[j]);
            }
        }
    }
    for(int i=1;i<=m;i++) if(op[i]==2) printf("%d\n",ans[i]);
    return 0;
}

AGC032F One Third

这么难我哪会啊……

题解。

  1. 在 \([0,1)\) 上随机取 \(n-1\) 个点,划分出来的 \(n\) 段长度最小值期望 \(\frac{1}{n^2}\)。
  2. 要钦定区间长度 \(\ge x\),考虑先把每段区间长度 \(-x\),后面再加回来(这个操作还是太有用了!)。
  3. 若连续随机变量 \(X\in[0,+\infty)\),$$E(X)=\int_0^{+\infty}P(X\ge x)dx$$ 感性理解或者分部积分均可证明。
  4. 对连续随机变量,概率函数不是概率,概率函数某一区间内的定积分才是概率。

P10681 [COTS 2024] 奇偶矩阵 Tablica

轻信了不知道谁说的建二分图,后面不会了,被提示了一下弱化版做法,笔者脑子抽了忘了有个东西叫容斥。

先考虑如果要求每行每列和恰为 \(2\) 怎么做(显然此时 \(n=m\))。考虑对每一列分配两个未知数表示 \(1\) 的位置,只需要把 \(n\) 种(\(2n\) 个)\(1\) 填进未知数里,方案数 \(\binom{2m}{2\ 2\ 2\dots 2}=\frac{(2m)!}{2^n}\)。但是现在有一列填的两个数相同的情况,需要容斥;而且每一列两个数应该无序,需要除以 \(2\)。所以这个弱化版的答案式为

\[ans=\frac{1}{2^m}\sum_{i=0}^m(-1)^i\binom{m}{i}\binom{n}{i}i!\frac{(2m-2i)!}{2^{n-i}} \]

接下来考虑原问题。考虑与上面类似地做,设置一个占位符表示不填数,额外的要求是同一列不能有两个占位符。设 \(f_{i,j}\) 表示 \(i\) 个位置填 \(j\) 种数方案数。答案式为

\[ans=\frac{1}{2^m}\sum_{i=0}^m(-1)^i\binom{m}{i}\sum_{j=0}^i\binom{n}{j}\binom{i}{j}j!f_{2(m-i),n-j} \]

\[f_{i,j}=\binom{i}{1}f_{i-1,j-1}+\binom{i}{2}f_{i-2,j-1} \]

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
typedef pair<int,int> pii;
typedef pair<int,ll> pil;
typedef pair<ll,int> pli;
typedef pair<ll,ll> pll;
template<typename T>
void chkmin(T &x,const T &y){x=min(x,y);}
template<typename T>
void chkmax(T &x,const T &y){x=max(x,y);}
const int inf=0x3f3f3f3f;
const ll infll=0x3f3f3f3f3f3f3f3f;
const int MOD=1000000007;
void add(int &x,int y){
    x+=y;
    if(x>=MOD) x-=MOD;
}
int qpow(int a,ll b){
    int mul=1;
    while(b){
        if(b&1) mul=(ll)mul*a%MOD;
        a=(ll)a*a%MOD;
        b>>=1;
    }
    return mul;
}
const int N=6005;
int n,m,f[N][N],fact[N],invfact[N];
void __INIT__(){
    fact[0]=1;
    for(int i=1;i<N;i++) fact[i]=(ll)i*fact[i-1]%MOD;
    invfact[N-1]=qpow(fact[N-1],MOD-2);
    for(int i=N-1;i>0;i--) invfact[i-1]=(ll)i*invfact[i]%MOD;
}
int C(int n,int m){
    if(n<m) return 0;
    return (ll)fact[n]*invfact[m]%MOD*invfact[n-m]%MOD;
}
int main(){
    __INIT__();
    scanf("%d%d",&n,&m);
    f[0][0]=1;
    for(int i=1;i<=(m<<1);i++){
        f[i][0]=1;
        for(int j=1;j<=n;j++){
            add(f[i][j],(ll)C(i,1)*f[i-1][j-1]%MOD);
            if(i>1) add(f[i][j],(ll)C(i,2)*f[i-2][j-1]%MOD);
        }
    }
    int ans=0;
    for(int i=0;i<=m;i++){
        int k=C(m,i);
        if(i&1) k=MOD-k;
        for(int j=0;j<=i;j++) add(ans,(ll)k*C(n,j)%MOD*C(i,j)%MOD*fact[j]%MOD*f[(m-i)<<1][n-j]%MOD);
    }
    printf("%lld\n",(ll)ans*qpow((MOD+1)>>1,m)%MOD);
    return 0;
}

AGC067D Unique Matching

前半部分看题解,后半部分自己做的。

观察到轮换对称性,所以钦定排列为 \(p_i=i\),在答案里乘上 \(n!\)。唯一对应一个排列可以转化为二分图存在唯一完美匹配,这意味着该二分图的完美匹配不存在“增广环”(从左到右走非匹配边,右到左走匹配边的二分图上环),将第 \(i\) 个左部点与第 \(i\) 个右部点合并,则图是一个 DAG。形式化地,连边 \(i\rightarrow [l_i,r_i]/i\),则得到的图是个 DAG。


(后面自己做的)

既然是个 DAG,考虑零度点容斥。设有 \(i\) 个点,钦定的零度点为 \(p_1,p_2,\dots,p_k\),则贡献为

\[(-1)^{k+1}(p_1+1)f_{p_1}\prod_{j=1}^{k-1}\left((p_{j+1}-p_j)^2f_{p_{j+1}-p_j-1}\right)(n-p_k+1)f_{n-p_k} \]

其中 \(f_i\) 表示 \(i\) 个点时的答案。

直接转移显然不行,设 \(g_j\) 表示最后一个点是零度点时带上容斥系数的贡献。

\[f_i=\sum_{j=1}^i(i-j+1)g_jf_{i-j} \]

\[g_i=-\sum_{j=1}^{i-1}j^2g_{i-j}f_{j-1}+if_{i-1} \]

答案:

\[ans=f_n\cdot n! \]

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
typedef pair<int,int> pii;
typedef pair<ll,int> pli;
typedef pair<int,ll> pil;
typedef pair<ll,ll> pll;
template<typename T>
void chkmin(T &x,const T &y){x=min(x,y);}
template<typename T>
void chkmax(T &x,const T &y){x=max(x,y);}
const int inf=0x3f3f3f3f;
const ll infll=0x3f3f3f3f3f3f3f3f;
int MOD;
void add(int &x,int y){
    x+=y;
    if(x>=MOD) x-=MOD;
}
int qpow(int a,ll b){
    int mul=1;
    while(b){
        if(b&1) mul=(ll)mul*a%MOD;
        a=(ll)a*a%MOD;
        b>>=1;
    }
    return mul;
}
const int N=5005;
int n,f[N],g[N];
int main(){
    scanf("%d%d",&n,&MOD);
    f[0]=1;
    for(int i=1;i<=n;i++){
        for(int j=1;j<=i;j++){
            if(j<i) add(g[i],MOD-(ll)j*j%MOD*g[i-j]%MOD*f[j-1]%MOD);
            else add(g[i],(ll)i*f[i-1]%MOD);
            add(f[i],(ll)(i-j+1)*g[j]%MOD*f[i-j]%MOD);
        }
    }
    int ans=f[n];
    for(int i=1;i<=n;i++) ans=(ll)ans*i%MOD;
    printf("%d\n",ans);
    return 0;
}

HDU6326 Monster Hunter (QOJ7588)

与本题类似,但强于该题,因为要求每个点都被访问。

用类似的套路,将点合并,变为一系列独立的点,记为 \((l,\Delta)\) 表示当 \(HP\ge l\) 时可获得 \(\Delta\) 增量。

考虑比较 \((l_1,\Delta_1)\) 与 \((l_2,\Delta_2)\) 优先度,若 \(\max(\Delta_1,\Delta_2)\ge 0\) 则比较方法与前述题目同理,否则,考虑倒过来,我们定的优先度显然是为了走得更远且让初始 \(HP\) 尽量小,因为如果全走完,\(HP\) 的变化量固定,没有优化的余地,这也说明最小化初始 \(HP\) 与最小化最终 \(HP\) 等价,故 \(\Delta<0\) 部分的比较在倒置后与 \(\Delta\ge 0\) 部分完全相同。

具体地,若 \((l_1,\Delta_1)\) 比 \((l_2,\Delta_2)\) 优,则有以下可能:

  • \(\Delta_1\ge 0>\Delta_2\),
  • \(\Delta_1,\Delta_2\ge 0\) 且 \(l_1<l_2\),
  • \(\Delta_1,\Delta_2<0\) 且 \(l_1+\Delta_1>l_2+\Delta_2\)。

dfs,到 \(u\) 时,将所有子树信息归并为一个堆,若 \(u\) 的二元组比堆顶劣,则将 \(u\) 与堆顶合并,并继续判断,直到堆为空或比堆顶优。合并方法与前述题目类似。

一种更简单的证明方法:

若选了 \(u\),选一个比 \(u\) 优的子结点 \(v\) 之前跳到了其他结点 \(w\),则 \(w\) 优于 \(v\),由于优劣关系有传递性,所以 \(w\) 比 \(u\) 先选,与 \(u\rightarrow w\rightarrow v\) 顺序矛盾。

由于 \(n\) 为 \(10^6\),不能启发式合并,需要用可并堆/平衡树,这里用了 pb_ds 的堆

#include<bits/stdc++.h>
#include<bits/extc++.h>
using namespace std;
typedef long long ll;
typedef pair<int,int> pii;
typedef pair<int,ll> pil;
typedef pair<ll,int> pli;
typedef pair<ll,ll> pll;
template<typename T>
void chkmin(T &x,const T &y){x=min(x,y);}
template<typename T>
void chkmax(T &x,const T &y){x=max(x,y);}
const int inf=0x3f3f3f3f;
const ll infll=0x3f3f3f3f3f3f3f3f;
const int N=100005;
struct Cmp{
    bool operator()(pll x,pll y)const{
        if(x.second>=0&&y.second<0) return false;
        if(x.second<0&&y.second>=0) return true;
        if(x.second>=0) return x.first>y.first;
        return x.first+x.second<y.first+y.second;
    }
};
typedef __gnu_pbds::priority_queue<pll,Cmp> prq;
int n;
ll a[N],b[N];
vector<int> G[N];
prq pq[N];
void dfs(int u,int f){
    for(auto v:G[u]){
        if(v==f) continue;
        dfs(v,u);
        pq[u].join(pq[v]);
    }
    pll p{a[u],b[u]-a[u]};
    for(;pq[u].size()&&Cmp{}(p,pq[u].top());pq[u].pop()){
        p.first=max(p.first,pq[u].top().first-p.second);
        p.second+=pq[u].top().second;
    }
    pq[u].push(p);
    // printf("u %d\n",u);
    // prq tmp=pq[u];
    // for(;tmp.size();tmp.pop()) printf("(%d,%d) ",tmp.top());
    // printf("\n");
}
void __INIT__(){}
void __CLEAR__(){
    for(int i=1;i<=n;i++) G[i].clear(),prq().swap(pq[i]);
}
void __SOLVE__(){
    scanf("%d",&n);
    __CLEAR__();
    for(int i=2;i<=n;i++) scanf("%lld%lld",&a[i],&b[i]);
    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);
    }
    dfs(1,1);
    ll x0=0,x=0;
    for(;pq[1].size();pq[1].pop()){
        if(x<pq[1].top().first){
            x0+=pq[1].top().first-x;
            x=pq[1].top().first;
        }
        x+=pq[1].top().second;
    }
    printf("%lld\n",x0);
}
int main(){
    int T;
    scanf("%d",&T);
    __INIT__();
    while(T--) __SOLVE__();
    return 0;
}

*CF773D Perishable Roads

被比大小击败了

原题描述等价于一颗内向树,考虑这棵树的形态有什么性质。

首先注意到这棵树一定是链+菊花,并且链底的边权是全局最小值。

证明:

全局最小值一定在最终的树上,否则可以连上这条边,断掉一条树边。

全局最小值下面的子树形态任意,因为任意调整贡献不变,故不妨调成菊花。

如果最终的树不是链+菊花,可以把不在上面的点都移到全局最小值下面,一定不劣。

进一步考虑链上边权关系,注意到有贡献的边一定是前缀 \(\min\),而如果一个点前后两个边均不是前缀 \(\min\),可以把这个点移到全局最小值下面,答案不会边劣。故链上两个前缀 \(\min\) 边权之间最多隔一条边。

更进一步,上面的结论还是不够好看,我们希望边权是单调的。称前缀 \(\min\) 所在边为关键边,否则为非关键边。考虑两个相邻非关键边,尝试合并,注意到这是可行的(笔者当时犯唐想到了但没意识到是更优的),如图:

不断调整,最终的树形态为一段边权单减的链+一条边权任意的边+一条全局最小值+菊花。

设链长为 \(i\),答案为 \(f+min\times(n-i)\) 考虑把 \(min\times i\) 放到链上,即每加一条边,贡献减 \(min\),因为边权单减,所以钦定链上点的贡献为其上面那条边(链底要特殊处理),从全局最小值开始 \(dijkstra\) 即可(有一些细节,不想写)。

P8321 『JROI-4』沈阳大街 2

贡献延迟计算板子。

注意到序列顺序不影响,把 \(A\) 和 \(B\) 均从大到小排序,对于 \(A\) 上一个点 \(i\),设其在 \(B\) 上 \(lower\_bound\) 为 \(l_i\),若 \(\pi_i\le l_i\),则 \(\min(A_i,B_{\pi_i})=A_i\),否则 \(=B_{\pi_i}\)。

考虑 DP,设 \(f_{i,j}\) 表示 \(A\) 前 \(i\) 个数还有 \(j\) 个 \(\min(A_k,B_{\pi_k})=B_{\pi_k}\) 且未确定 \(\pi_k\) 具体值。转移考虑有多少个 \(\pi_k\in(l_{i-1},l_i]\),以及当前点 \(\min\) 是什么。直接做复杂度应该就是 \(\mathcal{O}(n^2)\)。

贡献延迟计算适用于有某种对称性,或者说填数方案与位置不强相关,可以在未来考虑时(?

*ABC236Ex Distinct Multiples

笔者又忘了有个东西叫正难则反了。

考虑容斥。

重要结论:容斥可行的条件是任意大小恰为 \(i\) 的方案在 \(j<i\) 时均会被计算,比如在 主旋律 中,不能对零入度 scc 对应原图上点集就是因为有些大小为 \(3\) 的 scc 不会在 \(2\) 时被计算。

这里考虑对 \(A_i=A_j\) 的二元组 \((i,j)\) 容斥,目标是求出有 \(0\) 个这样的二元组方案数。考虑对这个直接做并不方便,转换一下,若将上述二元组的两点连边,可以得到一对连通分量,每个连通分量是一个等价类,\(A\) 都相等。

设 \(f_S\) 表示带容斥系数的,等价类 \(S\) 内部的贡献。非容斥系数的部分,方案数为

\[\lfloor\frac{m}{lcm_{i\in S}\{a_i\}}\rfloor \]

计算容斥系数是本题难点。根据定义,容斥系数是所有大小为 \(|S|\) 的连通图 \(-1\) 边数次方求和,形式化地,容斥系数为

\[\sum_{G=(S,E)\text{ is connected}}(-1)^{|E|} \]

正难则反,考虑拿总贡献减去不连通图贡献,求不连通图贡献,可以枚举 \(1\) 所在连通块,设上述式子 \(=p_{|S|}\)。

先推导总贡献

\[\sum_{j=0}^{\frac{i(i-1)}2}(-1)^j\binom{\frac{i(i-1)}2}{j}=[i=0\vee i=1] \]

则 \(p\) 的转移:

\[\begin{align*} p_i&=[i=0\vee i=1]-\sum_{j=1}^{i-1}\binom{i-1}{j-1}[i-j=0\vee i-j=1]p_j \\ &=[i=0\vee i=1]-\binom{i-1}{i-2}p_{i-1} \\ &=[i=0\vee i=1]-(i-1)p_{i-1} \end{align*} \]

归纳一下,可以证明 \(p_i=(-1)^{i-1}(i-1)!\),故

\[f_S=(-1)^{|S|-1}(|S|-1)!\lfloor\frac{m}{lcm_{i\in S}\{a_i\}}\rfloor \]

把 \(f\) 做集合幂级数 exp即可,如果不想集合幂级数 exp,也可以暴力求。复杂度 \(\mathcal{O}(2^n(n^2+\log m))\) 或 \(\mathcal{O}(3^n+2^n\log m)\),分别对应集幂和暴力。

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
typedef pair<int,int> pii;
typedef pair<int,ll> pil;
typedef pair<ll,int> pli;
typedef pair<ll,ll> pll;
typedef __int128_t int128;
template<typename T>
void chkmin(T &x,const T &y){x=min(x,y);}
template<typename T>
void chkmax(T &x,const T &y){x=max(x,y);}
const int inf=0x3f3f3f3f;
const ll infll=0x3f3f3f3f3f3f3f3f;
const int MOD=998244353;
void add(int &x,int y){
    x+=y;
    if(x>=MOD) x-=MOD;
}
int qpow(int a,ll b){
    int mul=1;
    while(b){
        if(b&1) mul=(ll)mul*a%MOD;
        a=(ll)a*a%MOD;
        b>>=1;
    }
    return mul;
}
const int N=20;
int fact[N],invfact[N],inv[N];
char cnt[1<<16];
int n,k[N],f[N][1<<16],g[N][1<<16];
ll m,d[N];
void __INIT__(){
    fact[0]=1;
    for(int i=1;i<N;i++) fact[i]=(ll)i*fact[i-1]%MOD;
    invfact[N-1]=qpow(fact[N-1],MOD-2);
    for(int i=N-1;i>0;i--) invfact[i-1]=(ll)i*invfact[i]%MOD;
    for(int i=1;i<N;i++) inv[i]=(ll)invfact[i]*fact[i-1]%MOD;
    for(int i=1;i<(1<<16);i++) cnt[i]=cnt[i>>1]+(i&1);
}
ll gcd(ll a,ll b){
    if(!b) return a;
    return gcd(b,a%b);
}
void fwt(int *f,int n,int k){
    for(int w=1;w<n;w<<=1)
        for(int i=0;i<n;i+=(w<<1))
            for(int j=0;j<w;j++) add(f[i+j+w],(ll)k*f[i+j]%MOD);
}
void exp(int c,int n){
    g[0][c]=1;
    for(int i=0;i<n;i++){
        for(int j=0;j<=i;j++) add(g[i+1][c],(ll)(i-j+1)*g[j][c]%MOD*f[i-j+1][c]%MOD);
        g[i+1][c]=(ll)g[i+1][c]*inv[i+1]%MOD;
    }
}
void __SOLVE__(){
    scanf("%d%lld",&n,&m);
    for(int i=1;i<=n;i++) scanf("%lld",&d[i]);
    k[1]=1;
    for(int i=2;i<=n;i++) k[i]=(MOD-(ll)(i-1)*k[i-1]%MOD)%MOD;
    // for(int i=1;i<=n;i++) printf("%d ",k[i]);printf("\n");
    for(int s=1;s<(1<<n);s++){
        ll lcm=1;
        for(int i=1;i<=n;i++) if(s>>(i-1)&1){
            ll g=gcd(lcm,d[i]);
            if((int128)lcm/g*d[i]>m){
                lcm=-1;
                break;
            }
            lcm=lcm/g*d[i];
        }
        if(lcm==-1) continue;
        f[cnt[s]][s]=m/lcm%MOD*k[cnt[s]]%MOD;
    }
    for(int i=0;i<=n;i++) fwt(f[i],1<<n,1);
    for(int j=0;j<(1<<n);j++) exp(j,n);
    for(int i=0;i<=n;i++) fwt(g[i],1<<n,MOD-1);
    printf("%d\n",g[n][(1<<n)-1]);
}
int main(){
    int T=1;
    __INIT__();
    while(T--) __SOLVE__();
    return 0;
}

*CF79D Password

虽然没做出来,但不算是很难的题。

区间 flip,考虑差分,变为每次 flip 一对点,考虑建图,一次 flip 相当于给这两个点连边,每个点有最终的度数限制。

注意到奇度点数量很少,想办法只对奇度点考虑,调整操作顺序,把偶度点上的操作两两配对,这样把所有操作看成一系列奇度点之间的路径。

考虑怎么找到所有可能的路径,笔者本来以为可以做背包,但是因为序列长度限制,操作没有平移对称性,直接从每个奇度点开始暴力 bfs,即可找到所有可能的路径。

然后状压,维护每个点奇偶性,DP 即可。

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
typedef pair<int,int> pii;
typedef pair<int,ll> pil;
typedef pair<ll,int> pli;
typedef pair<ll,ll> pll;
template<typename T>
void chkmin(T &x,const T &y){x=min(x,y);}
template<typename T>
void chkmax(T &x,const T &y){x=max(x,y);}
const int inf=0x3f3f3f3f;
const ll infll=0x3f3f3f3f3f3f3f3f;
const int N=10005,K=25,L=105;
int n,k,l,x[K],a[L],tot=0,w[K][K],dis[N],f[1<<20];
void bfs(int s){
    for(int i=1;i<=n+1;i++) dis[i]=inf;
    queue<int> q;
    q.push(s),dis[s]=0;
    while(!q.empty()){
        int u=q.front();
        q.pop();
        for(int i=1;i<=l;i++){
            if(u>a[i]&&dis[u-a[i]]>dis[u]+1){
                dis[u-a[i]]=dis[u]+1;
                q.push(u-a[i]);
            }
            if(u+a[i]<=n+1&&dis[u+a[i]]>dis[u]+1){
                dis[u+a[i]]=dis[u]+1;
                q.push(u+a[i]);
            }
        }
    }
}
int main(){
    scanf("%d%d%d",&n,&k,&l);
    for(int i=1;i<=k;i++){
        int tmp;
        scanf("%d",&tmp);
        if(x[tot]==tmp) x[tot]=tmp+1;
        else x[++tot]=tmp,x[++tot]=tmp+1;
    }
    for(int i=1;i<=l;i++) scanf("%d",&a[i]);
    for(int i=1;i<=tot;i++){
        bfs(x[i]);
        for(int j=i+1;j<=tot;j++) w[i][j]=dis[x[j]];
    }
    memset(f,0x3f,sizeof(f));
    f[0]=0;
    for(int s=0;s<(1<<tot);s++) for(int i=1;i<=tot;i++) for(int j=i+1;j<=tot;j++) chkmin(f[s^(1<<(i-1))^(1<<(j-1))],f[s]+w[i][j]);
    int ans=f[(1<<tot)-1];
    if(ans!=inf) printf("%d\n",ans);
    else printf("-1\n");
    return 0;
}

P2474 [SCOI2008] 天平

作为一道差分约束练习题还是不错的,但是个人认为自然的做法还是直接做。

翻遍题解区,找到唯一一篇相同做法,link,差点以为自己假了。

先把所有相等关系都缩到一起,如果存在 \(w_a>w_b>w_c\),则可唯一确定 \(w_a=3,w_b=2,w_c=1\)。排除以上情况后,如果有比 \(u\) 小的点,则 \(w_u\in\{2,3\}\);如果有比 \(u\) 大的点,则 \(w_u\in\{1,2\}\);否则 \(w_u\in\{1,2,3\}\)。

这样限制之后已经足够紧了。只关注 \(u,v\) 两点时,若二者之间没有直接大小关系,则所有符合上面条件的取值均可取到。对 \(a,b,c,d\) 四点同理,观摩题解后发现比较好写的做法是枚举可能的取值,再逐个判定二元关系是否满足,再判定天平倾斜情况是否唯一。


差分约束做法,移项比较还是很巧妙的。

*P3084 [USACO13OPEN] Photo G

正常做法

考虑 DP(笔者一开始被带偏了没想 DP,回来后立刻会了),设 \(f_i\) 表示 \(i\) 为已经考虑的最后一个 斑点奶牛 时,最多放多少头。转移考虑上一头 斑点奶牛 在哪,设为 \(j\),则既不能有区间同时包含 \(i,j\),也不能有区间包含于 \((i,j)\),其余限制都可以在 \(1\sim j\) 子问题中考虑。维护 \(jmax\) 表示含 \(i\) 区间左端点最小值,\(jmin\) 表示 \(i\) 左侧区间左端点最大值,则

\[f_i=\max\limits_{jmin\le j<jmax}\{f_j\}+1 \]

用线段树维护一下即可。

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
typedef pair<int,int> pii;
typedef pair<int,ll> pil;
typedef pair<ll,int> pli;
typedef pair<ll,ll> pll;
template<typename T>
void chkmin(T &x,const T &y){x=min(x,y);}
template<typename T>
void chkmax(T &x,const T &y){x=max(x,y);}
const int inf=0x3f3f3f3f;
const ll infll=0x3f3f3f3f3f3f3f3f;
const int N=200005;
int n,m,jmax[N],f[N],tr[N<<2];
pii seg[N];
void pushup(int x){
    tr[x]=max(tr[x<<1],tr[x<<1|1]);
}
void build(int x,int l,int r){
    if(l==r){
        tr[x]=f[l];
        return;
    }
    int mid=(l+r)>>1;
    build(x<<1,l,mid),build(x<<1|1,mid+1,r);
    pushup(x);
}
void update(int x,int l,int r,int p){
    if(l==r){
        tr[x]=f[p];
        return;
    }
    int mid=(l+r)>>1;
    if(p<=mid) update(x<<1,l,mid,p);
    if(p>mid) update(x<<1|1,mid+1,r,p);
    pushup(x);
}
int query(int x,int l,int r,int ql,int qr){
    if(ql<=l&&r<=qr) return tr[x];
    int mid=(l+r)>>1,mx=-inf;
    if(ql<=mid) chkmax(mx,query(x<<1,l,mid,ql,qr));
    if(qr>mid) chkmax(mx,query(x<<1|1,mid+1,r,ql,qr));
    return mx;
}
int main(){
    scanf("%d%d",&n,&m);
    for(int i=1;i<=m;i++) scanf("%d%d",&seg[i].first,&seg[i].second);
    sort(seg+1,seg+m+1,[&](pii x,pii y){return x.second<y.second;});
    int mn=n;
    for(int i=n,j=m;i>=1;i--){
        for(;j>=1&&seg[j].second>=i;j--) chkmin(mn,seg[j].first);
        jmax[i]=mn;
    }
    for(int i=1;i<=n;i++) f[i]=-inf;
    f[0]=0;
    build(1,0,n);
    int jmin=0;
    for(int i=1,j=1;i<=n;i++){
        for(;j<=m&&seg[j].second<i;j++) chkmax(jmin,seg[j].first);
        if(jmin>=jmax[i]) continue;
        f[i]=query(1,0,n,jmin,jmax[i]-1)+1;
        update(1,0,n,i);
    }
    for(int j=1;j<=m;j++) chkmax(jmin,seg[j].first);
    int ans=query(1,0,n,jmin,n);
    if(ans<0) printf("-1\n");
    else printf("%d\n",ans);
    return 0;
}

神秘做法

转化是厉害的(我不会),爆冲是难绷的。

不难发现问题等价于

\[\begin{aligned} \max\limits_{i=1}^n\; & \sum\limits_{i=1}^nx_i \\ \text{subject to } & \forall 1\le i\le m,\sum\limits_{j=l_i}^{r_i}x_j=1, \\ & \forall 1\le i\le n,x_i\in\{0,1\}. \end{aligned} \]

注意到都是区间的形式,使用经典套路——前缀和,变为

\[\begin{aligned} \max\limits_{i=1}^n\; & s_n \\ \text{subject to } & \forall 1\le i\le m,s_{r_i}-s_{l_i-1}=1, \\ & \forall 0\le i<n,s_{i+1}-s_i\ge 0, \\ & \forall 0\le i<n,s_{i+1}-s_i\le 1, \\ & s_0=0. \end{aligned} \]

这是个差分约束的形式,直接跑。有一个结论是,差分约束跑出来相对 \(s\) 点就是最大的,因为每个点都卡到了上界。

据说用双段队列优化一下就行。

P3530 [POI 2012] FES-Festival

读错题被卡了 1h。

本人做法十分诡异,不过根大众做法应该是等价的。

不难想到建图,一个人是一个点,用时是其点权。不管是哪种限制,都能被弱化为大小关系,考虑从 \(u\) 向 \(x_v\le x_u\) 的 \(v\) 连边,进行第一次缩点,对于每一个 scc,其内部的点权值应该相同,如果内部有第一类限制,就炸了。

接下来,发现第一类限制很强,考虑现保证第一类限制,只保留第一类限制产生的边,原图可被分成若干连通块,且每个连通块内部相对权值固定,可被拍扁成一条链,且不考虑第二类限制时,链(在数轴上的投影,后省略)之间可以任意偏移,最大化成绩种数即最大化所有链合在一起时的长度,可以发现我们要求有一个链顶头,且链之间没有空隙。

接下来考虑第二类限制,这相当于限制了链之间的相对位置,我们想直接跑差分约束,但是不行,因为这会导致出现空隙。观察一下,如果两个链 \(A,B\) 之间只有 \(A\rightarrow B\) 的边,显然把 \(A\) 尽量后移是不劣的,而如果互相可达,则不会导致出现缝隙,可以直接差分约束。故第二次缩点,每个 scc 内跑差分约束,scc 之间拓扑排序,按顺序首尾相接拼起来。

看题解发现小 trick:这里是“全源”差分约束,即求任意两点之间最短路最大值,直接跑 floyd 即可。


更聪明的做法,学习自本篇。

直接建差分约束的图,缩点(其实可以不缩,因为对最短路不产生影响),和上面类似的原理,scc 内部直接差分约束求答案,再把所有 scc 答案简单求和。

*CF436E Cardboard Box

成功学习 012 背包。

考虑反悔贪心,背包容量加 \(1\) 时,有以下可能:

  • \(0\rightarrow 1\),即加入一个 \(1\)。
  • \(1\rightarrow 2\),即把 \(1\) 改为对应的 \(2\)。
  • \(1\rightarrow 0,0\rightarrow 2\),把一个 \(1\) 删掉,加上另一个 \(2\)。与上一个区别是,这个 \(1\) 和 \(2\) 对应物品不同,上一个反之。
  • \(2\rightarrow 1,0\rightarrow 2\)。

可能比较自然的发现顺序:

首先转移 \(1\) 是易得的。然后考虑如何加进来 \(2\),得到转移 \(2,3\)。接下来,注意到 \(0,1\) 都是可能变为其他状态的,而 \(2\) 不行,但真的没有转移吗?不然,于是得到转移 \(4\)。

用 \(5\) 个小根堆维护 \(0\rightarrow 1,0\rightarrow 2,1\rightarrow 2,1\rightarrow 0,2\rightarrow 1\),状态改变时惰性删除。

总结:反悔贪心不一定要有凸性,反悔贪心的物品状态可以反复横跳。

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
typedef pair<int,int> pii;
typedef pair<int,ll> pil;
typedef pair<ll,int> pli;
typedef pair<ll,ll> pll;
template<typename T>
void chkmin(T &x,const T &y){x=min(x,y);}
template<typename T>
void chkmax(T &x,const T &y){x=max(x,y);}
const int inf=0x3f3f3f3f;
const ll infll=0x3f3f3f3f3f3f3f3f;
const int MOD=998244353;
void add(int &x,int y){
    x+=y;
    if(x>=MOD) x-=MOD;
}
int qpow(int a,ll b){
    int mul=1;
    while(b){
        if(b&1) mul=(ll)mul*a%MOD;
        a=(ll)a*a%MOD;
        b>>=1;
    }
    return mul;
}
const int N=300005;
int n,w,vis[N];
ll a[N],b[N];
priority_queue<pli,vector<pli>,greater<pli>> pq1,pq2,pq3,pq4,pq5;
void ins0(int p){
    vis[p]=0;
    pq1.push({a[p],p}),pq2.push({b[p],p});
}
void ins1(int p){
    vis[p]=1;
    pq3.push({b[p]-a[p],p}),pq4.push({-a[p],p});
}
void ins2(int p){
    vis[p]=2;
    pq5.push({a[p]-b[p],p});
}
void __INIT__(){}
void __SOLVE__(){
    scanf("%d%d",&n,&w);
    for(int i=1;i<=n;i++){
        scanf("%lld%lld",&a[i],&b[i]);
        ins0(i),vis[i]=0;
    }
    ll ans=0;
    for(int i=1;i<=w;i++){
        while(pq1.size()&&vis[pq1.top().second]!=0) pq1.pop();
        while(pq2.size()&&vis[pq2.top().second]!=0) pq2.pop();
        while(pq3.size()&&vis[pq3.top().second]!=1) pq3.pop();
        while(pq4.size()&&vis[pq4.top().second]!=1) pq4.pop();
        while(pq5.size()&&vis[pq5.top().second]!=2) pq5.pop();
        pli mn{infll,0};
        if(pq1.size()) chkmin(mn,{pq1.top().first,1});
        if(pq3.size()) chkmin(mn,{pq3.top().first,2});
        if(pq4.size()&&pq2.size()) chkmin(mn,{pq4.top().first+pq2.top().first,3});
        if(pq5.size()&&pq2.size()) chkmin(mn,{pq5.top().first+pq2.top().first,4});
        assert(mn.second);
        ans+=mn.first;
        if(mn.second==1){
            int p=pq1.top().second;
            pq1.pop();
            ins1(p);
        }
        else if(mn.second==2){
            int p=pq3.top().second;
            pq3.pop();
            ins2(p);
        }
        else{
            int q=pq2.top().second;
            pq2.pop();
            ins2(q);
            if(mn.second==3){
                int p=pq4.top().second;
                pq4.pop();
                vis[p]=0;
                ins0(p);
            }
            else{
                int p=pq5.top().second;
                pq5.pop();
                ins1(p);
            }
        }
        // printf("%lld\n",ans);
        // for(int i=1;i<=n;i++) printf("%d",vis[i]);printf("\n");
    }
    printf("%lld\n",ans);
    for(int i=1;i<=n;i++) printf("%d",vis[i]);printf("\n");
}
int main(){
    int T=1;
    // scanf("%d",&T);
    __INIT__();
    while(T--) __SOLVE__();
    return 0;
}

P5356 [Ynoi Easy Round 2017] 由乃打扑克

怎么题目底下写了复杂度啊……不知道自己想能不能想到带根号。

既然提示了带根号,考虑分块,整块修改直接大标记,散块修改直接归并。查询考虑二分答案,变为计算 rank,再进行一个二分,散块需要提前把数组算出来,可能需要用个链表之类的维护。这样(我以为)复杂度是 \(\mathcal{O}(n(B+\frac{nlog^2n}{B})\),平衡一下得到 \(\mathcal{O}(n\sqrt{n}\log n)\)。

发现完全不会如何把 \(\log\) 放到根号里面,打开题解一看,分散层叠。再一看,其他做法要优化二分次数。

优化二分次数,注意到外层每次 \(check\) 完,内层二分的范围会缩小,对着缩小后的范围二分做,以及如果在内层二分前就已经知道 rank,就不二分了。

*ARC120F Wine Thief

厉害题。不敢列出暴力计算式导致的。

先看 \(n\) 个位置选 \(k\) 个不相邻的方案数是什么,是这个东西,即 \(\binom{n-k+1}{k}\)。

拆贡献,考虑计算一个位置被选中的方案数。

法一:正常做法

正难则反(其实没必要,不过我懒得重新推式子了),统计 \(i\) 不选的方案数。考虑组合意义,考虑删除 \(i\) 这个数算答案,即 \(\binom{n-1-k+1}{k}\)。但这样会漏统计同时选上 \(i-1\) 和 \(i+1\) 的方案(笔者当时到这步卡住不会了),考虑钦定 \(i-1\) 和 \(i+1\) 必选,这样 \(i-2\) 和 \(i+2\) 必不选,方案数为 \(\binom{n-5-(k-2)+1}{k-2}\)。再钦定 \(i-3,i+3\) 必选……不断这样做,选 \(i\) 的总方案数为

\[\binom{n-k+1}{k}-\sum\limits_{j=0}^{lim_i}\binom{n-4j-1-(k-2j)+1}{k-2j}+\Delta_i \]

\(\Delta_i\) 为边界情况,即 \(i\pm 2j\) 超出边界时的情况。注意到求和式只有枚举上界与 \(i\) 有关,前缀和预处理即可。

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
typedef pair<int,int> pii;
typedef pair<int,ll> pil;
typedef pair<ll,int> pli;
typedef pair<ll,ll> pll;
const int MOD=998244353;
void add(int &x,int y){
    x+=y;
    if(x>=MOD) x-=MOD;
}
int qpow(int a,ll b){
    int mul=1;
    while(b){
        if(b&1) mul=(ll)mul*a%MOD;
        a=(ll)a*a%MOD;
        b>>=1;
    }
    return mul;
}
const int N=300005;
int n,k,d,a[N],fact[N],invfact[N],sum[N];
int C(int n,int m){
    if(m<0||n<m) return 0;
    return (ll)fact[n]*invfact[m]%MOD*invfact[n-m]%MOD;
}
void __INIT__(){
    fact[0]=1;
    for(int i=1;i<N;i++) fact[i]=(ll)i*fact[i-1]%MOD;
    invfact[N-1]=qpow(fact[N-1],MOD-2);
    for(int i=N-1;i>0;i--) invfact[i-1]=(ll)i*invfact[i]%MOD;
}
void __SOLVE__(){
    scanf("%d%d%d",&n,&k,&d);
    for(int i=1;i<=n;i++) scanf("%d",&a[i]);
    for(int i=0;i<=n;i++){
        if((i<<2)<n&&(i<<1)<=k) sum[i]=C(n-(i<<2)-1-(k-(i<<1))+1,k-(i<<1));
        if(i) add(sum[i],sum[i-1]);
    }
    // for(int i=1;i<=n;i++) printf("%d ",sum[i]);printf("\n");
    int ans=0;
    for(int i=1;i<=n;i++){
        // printf("i=%d\n",i);
        int s=C(n-k+1,k),len=min(i-1,n-i);
        add(s,MOD-sum[len>>1]);
        if(len&1) add(s,MOD-C(max(n-(len<<1)-2,0)-(k-len-1)+1,k-len-1));
        // printf("%d\n",s);
        add(ans,(ll)a[i]*s%MOD);
    }
    printf("%d\n",ans);
}
int main(){
    __INIT__();
    __SOLVE__();
    return 0;
}

法二:多项式科技

???+ 折叠优化体验

学习自这个讨论

\[\sum\limits_{i=1}^nA_i\sum\limits_{j=0}^{k-1}\binom{i-1-j}{j}\binom{n-i-(k-j-1)}{k-j-1} \]

注意到所有 \(A_i\) 为求和关系(且 \(A_i\) 系数为组合数,可以写成 D-Finite 的整式递推),设哑元 \(t\),用 \(t^i\) 代替 \(A_i\)。

\[\begin{align*} &\sum\limits_{i=1}^nt^i\sum\limits_{j=0}^{k-1}\binom{i-1-j}{j}\binom{n-i-(k-j-1)}{k-j-1} \\ =&\sum\limits_{j=0}^{k-1}\sum\limits_{i=1}^nt^i\binom{i-1-j}{j}\binom{n-i-(k-j-1)}{k-j-1} \\ =&\sum\limits_{j=0}^{k-1}\sum\limits_{i=1}^nt^i\binom{i-j-1}{i-2j-1}\binom{n-i-(k-j-1)}{n-i-2(k-j-1)} \\ =&\sum\limits_{j=0}^{k-1}\sum\limits_{i=1}^nt^i(-1)^{i-2j-1}\binom{-j-1}{i-2j-1}(-1)^{n-i-2(k-j-1)}\binom{-(k-j-1)-1}{n-i-2(k-j-1)} \\ =&\sum\limits_{j=0}^{k-1}\sum\limits_{i=1}^nt^i[x^{i-2j-1}](1-x)^{-j-1}[x^{n-i-2(k-j-1)}](1-x)^{-(k-j-1)-1} \\ =&\sum\limits_{j=0}^{k-1}\sum\limits_{i=1}^nt^i[x^i]\frac{x^{2j+1}}{(1-x)^{j+1}}[x^{n-i}]\frac{x^{2(k-j-1)}}{(1-x)^{k-j}} \end{align*} \]

我们想让其变成卷积,但现在 \(t^i\) 很烦人,但注意到 \([x^i]\frac{x^{2j+1}}{(1-x)^{j+1}}\) 中 \(x\) 指数恰为 \(i\),考虑令 \(x\leftarrow xt\),这样 \(x^i\) 系数自动被乘上了 \(t^i\)。

\[\begin{align*} &\sum\limits_{j=0}^{k-1}\sum\limits_{i=1}^nt^i[x^i]\frac{x^{2j+1}}{(1-x)^{j+1}}[x^{n-i}]\frac{x^{2(k-j-1)}}{(1-x)^{k-j}} \\ =&\sum\limits_{j=0}^{k-1}\sum\limits_{i=1}^n[x^i]\frac{(xt)^{2j+1}}{(1-xt)^{j+1}}[x^{n-i}]\frac{x^{2(k-j-1)}}{(1-x)^{k-j}} \\ =&\sum\limits_{j=0}^{k-1}\sum\limits_{i=1}^n[x^i]\frac{(xt)^{2j+1}}{(1-xt)^{j+1}}[x^{n-i}]\frac{x^{2(k-j-1)}}{(1-x)^{k-j}} \\ \end{align*} \]

直接卷积,不需要关心 \(i\) 的范围,因为我们最终只关心 \(t^1\sim t^n\) 项。

\[\begin{align*} &\sum\limits_{j=0}^{k-1}\sum\limits_{i=1}^n[x^i]\frac{(xt)^{2j+1}}{(1-xt)^{j+1}}[x^{n-i}]\frac{x^{2(k-j-1)}}{(1-x)^{k-j}} \\ =&\sum\limits_{j=0}^{k-1}[x^n]\frac{(xt)^{2j+1}}{(1-xt)^{j+1}}\frac{x^{2(k-j-1)}}{(1-x)^{k-j}} \\ =&\sum\limits_{j=0}^{k-1}[x^n]\frac{(xt)^{2j+1}}{(1-xt)^{j+1}}\frac{x^{2(k-j-1)}}{(1-x)^{k-j}} \\ =&[x^n]\sum\limits_{j=0}^{k-1}\frac{(xt)^{2j+1}}{(1-xt)^{j+1}}\frac{x^{2(k-j-1)}}{(1-x)^{k-j}} \\ =&[x^n]\sum\limits_{j=0}^{k-1}\frac{t^{2j+1}x^{2k-1}}{(1-xt)^{j+1}(1-x)^{k-j}} \\ =&[x^n]\frac{tx^{2k-1}}{(1-xt)(1-x)^k}\frac{1-\frac{t^{2k}(1-x)^k}{(1-xt)^k}}{1-\frac{t^2(1-x)}{(1-xt)}} \\ =&[x^{n-2k+1}]\frac{t-t^{2k+1}(1-x)^k(1-xt)^{-k}}{(1-xt)(1-x)^k-t^2(1-x)^{k+1}} \\ =&[x^{n-2k+1}]\frac{t(1-x)^{-k}-t^{2k+1}(1-xt)^{-k}}{(1-xt)-t^2(1-x)} \\ =&[x^{n-2k+1}]\frac{t((1-x)^{-k}-t^{2k}(1-xt)^{-k})}{(1-t)(1+t(1-x))} \\ =&\frac{t}{1-t}[x^{n-2k+1}]\frac{(1-x)^{-k}-t^{2k}(1-xt)^{-k}}{1+t(1-x)} \\ \end{align*} \]

由于我们想快速求,考虑把分母拆成 \((r-x)^k\) 的形式。

考虑分母的零点,此时分子需要被分母整除,即也是零。令 \(1+t(1-x)=0\),解得 \(x=1+\frac1t\)。代入 \((1-x)^{-k}\),令其为 \(0\),需要 \(-(-t)^{k}\),同理,另一项要 \(+(-t)^{k}\)。

\[\begin{align*} &\frac{t}{1-t}[x^{n-2k+1}]\frac{(1-x)^{-k}-t^{2k}(1-xt)^{-k}}{1+t(1-x)} \\ =&\frac{t}{1-t}[x^{n-2k+1}]\frac{(1-x)^{-k}-(-t)^k}{1+t(1-x)}+\frac{(-t)^k-t^{2k}(1-xt)^{-k}}{1+t(1-x)} \\ =&\frac{t}{1-t}[x^{n-2k+1}]\frac{\frac{1}{(1-x)^k}-(-t)^k}{(1-x)(\frac{1}{1-x}-(-t))}+\frac{(-t)^k-\frac{t^{2k}}{(1-xt)^k}}{-\frac{1-xt}{t}(-t-\frac{t^2}{1-xt})} \\ =&\boxed{\frac{t}{1-t}[x^{n-2k+1}]\sum_{i=0}^{k-1}\frac{(-t)^i}{(1-x)^{k-i}}+\frac{(-t)^{k+i}}{(1-xt)^{i+1}}} \\ \end{align*} \]

最后一步运用了

\[\frac{u^k-v^k}{u-v}=\sum_{i=0}^{k-1}u^{k-i-1}v^i \]

这样,每个 \(i\) 只对 \(t\) 的系数做 \(\mathcal{O}(1)\) 次修改,可以 \(\mathcal{O}(n)\) 处理。

???-

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
typedef pair<int,int> pii;
typedef pair<int,ll> pil;
typedef pair<ll,int> pli;
typedef pair<ll,ll> pll;
const int MOD=998244353;
void add(int &x,int y){
    x+=y;
    if(x>=MOD) x-=MOD;
}
int qpow(int a,ll b){
    int mul=1;
    while(b){
        if(b&1) mul=(ll)mul*a%MOD;
        a=(ll)a*a%MOD;
        b>>=1;
    }
    return mul;
}
const int N=300005;
int n,k,d,a[N],fact[N],invfact[N],f[N];
int C(int n,int m){
    if(m<0||n<m) return 0;
    return (ll)fact[n]*invfact[m]%MOD*invfact[n-m]%MOD;
}
void __INIT__(){
    fact[0]=1;
    for(int i=1;i<N;i++) fact[i]=(ll)i*fact[i-1]%MOD;
    invfact[N-1]=qpow(fact[N-1],MOD-2);
    for(int i=N-1;i>0;i--) invfact[i-1]=(ll)i*invfact[i]%MOD;
}
void __SOLVE__(){
    scanf("%d%d%d",&n,&k,&d);
    for(int i=1;i<=n;i++) scanf("%d",&a[i]);
    int tmp=n-(k<<1)+1;
    // printf("tmp %d\n",tmp);
    for(int i=0;i<k;i++){
        int coef=1;
        if(i&1) coef=MOD-1;
        // printf("%d %d %d\n",i,coef,C(k-i+tmp-1,tmp));
        add(f[i],(ll)coef*C(k-i+tmp-1,tmp)%MOD);
        if(k&1) coef=MOD-coef;
        // printf("%d %d %d\n",i+tmp,coef,C(i+tmp,tmp));
        add(f[i+k+tmp],(ll)coef*C(i+tmp,tmp)%MOD);
    }
    // for(int i=0;i<=n;i++) printf("%d ",f[i]);printf("\n");
    int ans=0;
    for(int i=1;i<=n;i++){
        add(ans,(ll)f[i-1]*a[i]%MOD);
        add(f[i],f[i-1]);
    }
    printf("%d\n",ans);
}
int main(){
    __INIT__();
    __SOLVE__();
    return 0;
}
posted @ 2026-01-10 11:33  SmpaelFx  阅读(58)  评论(0)    收藏  举报