做题记录 2026.02

*CF1508F Optimal Encoding

抛开代码实现不谈,这是一道好题。

题意的人话是,对每个区间排序,把结果连成一条链,所有链求并后,反向传递闭包后的边数是多少?

(后面看了题解)

套路化地想到考虑每条边出现时间,一条边 \((u,v)\) 出现当且仅当有区间包含 \(u,v\) 且没有区间包含 \(u,v,w\)(其中 \(a_u<a_w<a_v\)),这是 2-side 矩形查 \(\min\),可以离线下来二维数点(具体做法后面再说)。

这样获得一个 \(\mathcal{O}(n^2+\varepsilon)\) 做法。

瓶颈在于要对每条边考虑,感受一下,发现有贡献的边不应该太多,进而注意到莫队的结论。具体地,有贡献的边一定在某个区间对应的链上,考虑对 \(q\) 个区间做莫队,用链表维护我们当前的链,这样有贡献的边数是 \(\mathcal{O}(n\sqrt{q})\) 级别。

用莫队找到有贡献的边。再用扫描线做 3-side 矩形查询最值,找到 \((u,v)\) 两侧的 \(w_1,w_2\)(注意 \(u,v\) 之间也可能有 \(w\))。再离线下来用扫描线做 2-side 矩形查最值,找到边的出现时间,打差分标记最后前缀和起来即可。

为了做到不带 \(\log\),需要用分块套 ST 表(ST 表可以 \(\mathcal{O}(len)\) 修改),总复杂度 \(\mathcal{O}(n\sqrt{q}+q\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=25005,Q=100005;
int n,q,a[N],tim[8000005][3],ans[Q];
pii qs[Q];
vector<pii> es;
namespace modui{
    const int B=80;
    // const int B=2;
    int pre[N],nxt[N];
    pii tmp[Q];
    stack<pair<int*,int>> st;
    int bid(int x){
        return (x-1)/B+1;
    }
    bool cmp(pii x,pii y){
        int a=bid(x.first),b=bid(y.first);
        if(a!=b) return a<b;
        return x.second>y.second;
    }
    void del(int i,bool mem=false){
        if(mem) st.push({&nxt[pre[i]],i}),st.push({&pre[nxt[i]],i});
        nxt[pre[i]]=nxt[i],pre[nxt[i]]=pre[i];
        if(pre[i]&&nxt[i]) es.push_back({min(pre[i],nxt[i]),max(pre[i],nxt[i])});
    }
    void undo(){
        *st.top().first=st.top().second;
        st.pop();
    }
    void solve(){
        memcpy(tmp+1,qs+1,sizeof(pii)*q);
        sort(tmp+1,tmp+q+1,cmp);
        vector<pii> vec;
        for(int i=1;i<=n;i++) vec.push_back({a[i],i});
        sort(vec.begin(),vec.end());
        pre[vec[0].second]=nxt[vec.back().second]=0;
        nxt[0]=vec[0].second,pre[0]=vec.back().second;
        for(int i=1;i<vec.size();i++){
            int u=vec[i-1].second,v=vec[i].second;
            es.push_back({min(u,v),max(u,v)});
        }
        int l,r,L;
        for(int i=1;i<=q;i++){
            if(i==1||bid(tmp[i].first)!=bid(tmp[i-1].first)){
                l=1,r=n,L=(bid(tmp[i].first)-1)*B+1;
                for(int i=1;i<vec.size();i++) nxt[vec[i-1].second]=vec[i].second,pre[vec[i].second]=vec[i-1].second;
                while(l<L) del(l++);
            }
            while(r>tmp[i].second) del(r--);
            while(l<tmp[i].first) del(l++,1);
            while(st.size()) undo();
            l=L;
        }
    }
}
template<typename T,typename Func>
void sort(vector<T> &v,const Func &f){
    vector<vector<T>> buc(n+5);
    for(auto &i:v) buc[f(i)].push_back(i);
    v.clear();
    for(int i=1;i<=n;i++) for(auto &j:buc[i]) v.push_back(j);
}
struct Hsh{
    ll operator()(pii p)const{
        return p.first*n+p.second;
    }
};
void unique(vector<pii> &v){
    unordered_set<pii,Hsh> vis;
    vector<pii> tmp;
    for(auto i:v) if(!vis.count(i)) vis.insert(i),tmp.push_back(i);
    v.swap(tmp);
}
namespace rmq{
    const int B=160;
    // const int B=2;
    template<int L>
    struct ST{
        int st[L][10];
        void build(int v){
            for(int i=0;i<10;i++) for(int j=0;j+(1<<i)<=L;j++) st[j][i]=v;
        }
        void modify(int x,int v){
            if(v>=st[x][0]) return;
            for(int i=0;i<10;i++) for(int j=max(x-(1<<i)+1,0);j<=x;j++) chkmin(st[j][i],v);
        }
        int query(int l,int r){
            int lg=31^__builtin_clz(r-l+1);
            return min(st[l][lg],st[r-(1<<lg)+1][lg]);
        }
    };
    ST<B+5> part[N/B+5];
    ST<N/B+5> full;
    int bid(int x){
        return x/B;
    }
    void build(int v){
        full.build(v);
        for(int i=0,imax=bid(n+1);i<=imax;i++) part[i].build(v);
    }
    void insert(int x,int v){
        int tmp=bid(x);
        full.modify(tmp,v);
        part[tmp].modify(x-tmp*B,v);
    }
    int query(int l,int r){
        int k1=bid(l),k2=bid(r);
        if(k1==k2) return part[k1].query(l-k1*B,r-k1*B);
        int res=inf;
        chkmin(res,part[k1].query(l-k1*B,B-1));
        chkmin(res,part[k2].query(0,r-k2*B));
        if(k1<k2-1) chkmin(res,full.query(k1+1,k2-1));
        return res;
    }
}
vector<pair<pii,pii>> pts;
template<typename T>
void clear(T &x){
    T().swap(x);
}
void __INIT__(){}
void __SOLVE__(){
    scanf("%d%d",&n,&q);
    for(int i=1;i<=n;i++) scanf("%d",&a[i]);
    for(int i=1;i<=q;i++) scanf("%d%d",&qs[i].first,&qs[i].second);
    modui::solve();
    sort(es,[&](pii x){return x.second;});
    unique(es);
    int tot=es.size();
    pts.reserve(tot*3+q);
    for(int i=1;i<=q;i++) pts.push_back({qs[i],{i,-1}});
    // for(auto [u,v]:es) printf("(%d,%d)\n",u,v);
    rmq::build(n+1);
    for(int i=0,j=1;i<tot;i++){
        int u=es[i].first,v=es[i].second,L=min(a[u],a[v]),R=max(a[u],a[v]);
        for(;j<v;j++) rmq::insert(a[j],n-j+1);
        pts.push_back({{u,v},{i,0}});
        if(L<R-1) pts.push_back({{n-rmq::query(L+1,R-1)+1,v},{i,1}});
    }
    rmq::build(n+1);
    for(int i=tot-1,j=n;i>=0;i--){
        int u=es[i].first,v=es[i].second,L=min(a[u],a[v]),R=max(a[u],a[v]);
        for(;j>v;j--) rmq::insert(a[j],j);
        if(L<R-1) pts.push_back({{u,rmq::query(L+1,R-1)},{i,2}});
    }
    clear(es);
    // for(auto [p,q]:pts) printf("(%d,%d,%d,%d) ",p.first,p.second,q.first,q.second);
    // printf("\n");
    sort(pts,[&](pair<pii,pii> p){return p.first.first;});
    rmq::build(q+1);
    for(int i=0;i<tot;i++) tim[i][0]=tim[i][1]=tim[i][2]=q+1;
    for(auto [p,q]:pts){
        if(q.second==-1) rmq::insert(p.second,q.first);
        else tim[q.first][q.second]=rmq::query(p.second,n+1);
    }
    for(int i=0;i<tot;i++){
        // printf("%d %d %d\n",tim[i][0],tim[i][1],tim[i][2]);
        int t1=tim[i][0],t2=min(tim[i][1],tim[i][2]);
        if(t1<t2) ans[t1]++,ans[t2]--;
    }
    for(int i=1;i<=q;i++){
        ans[i]+=ans[i-1];
        printf("%d\n",ans[i]);
    }
}
int main(){
    int T=1;
    // scanf("%d",&T);
    __INIT__();
    while(T--) __SOLVE__();
    return 0;
}

P4819 [中山市选] 杀人游戏

显然要建图,从一个人开始问,显然可以推到这个人可达的所有人。可达性,想到缩点,对于 DAG,不难发现从零度点开始一定最优,并且每个零度点都要被“冒风险”询问一次。有一个细节,注意到还有一种找到嫌犯的方法叫排除法,这种可用当且仅当有一个零度点(非 DAG 缩点后大小为 \(1\))所有出度都可被其他点达到。答案为 \(1-\frac{cnt}{n}\) 或 \(1-\frac{cnt-1}{n}\),其中 \(cnt\) 为零度点/scc 数量。

#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 int N=100005;
int n,m,dfn[N],low[N],tot,instack[N],id[N],sz[N],deg[N];
vector<int> G[N],H[N];
stack<int> st;
void dfs(int u){
    dfn[u]=low[u]=++tot,instack[u]=1,st.push(u);
    for(auto v:G[u]){
        if(!dfn[v]){
            dfs(v);
            chkmin(low[u],low[v]);
        }
        else if(instack[v]) chkmin(low[u],dfn[v]);
    }
    if(dfn[u]==low[u]){
        while(st.top()!=u){
            int v=st.top();
            st.pop();
            instack[v]=0,id[v]=u,sz[u]++;
        }
        instack[u]=0,id[u]=u,sz[u]++;
        st.pop();
    }
}
bool check(int u){
    for(auto v:H[u]) if(deg[v]==1) return false;
    return true;
}
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);
    }
    // dfs(1);
    for(int i=1;i<=n;i++) if(!dfn[i]) dfs(i);
    // for(int i=1;i<=n;i++) printf("%d ",dfn[i]);printf("\n");
    // for(int i=1;i<=n;i++) printf("%d ",low[i]);printf("\n");
    for(int i=1;i<=n;i++) for(auto j:G[i]) if(id[i]!=id[j]) H[id[i]].push_back(id[j]);
    for(int i=1;i<=n;i++) if(id[i]==i){
        sort(H[i].begin(),H[i].end());
        H[i].erase(unique(H[i].begin(),H[i].end()),H[i].end());
        for(auto j:H[i]) deg[j]++;
    }
    vector<int> vec;
    for(int i=1;i<=n;i++) if(id[i]==i&&deg[i]==0) vec.push_back(i);
    // printf("%d\n",vec.size());
    for(auto u:vec){
        if(sz[u]==1&&check(u)){
            printf("%.6f\n",1-(double)(vec.size()-1)/n);
            return 0;
        }
    }
    printf("%.6f\n",1-(double)vec.size()/n);
    return 0;
}

*AGC044E Random Pawn

很优雅的一道题。

很像一维随机游走,(上网查了一下后)列出方程:

\[E_i=\max(A_i,\frac{E_{i-1}+E_{i+1}}2-B_i) \]

环上很丑,观察一下(如考虑 \(B_i=0\) 特殊情况),发现在 \(A\) 最大值处一定会停下,故在最大值处断环成链,令 \(A_0=A_n=A_{\max}\)。

(后面看了题解)

想办法把 \(B_i\neq 0\) 的情况转为 \(B_i=0\) 的情况,设一个 \(E_i=E'_i+\Delta_i\),且满足

\[E'_i=\max(A_i-\Delta_i,\frac{E'_{i-1}+E'_{i+1}}2) \]

则

\[\Delta_i=\frac{\Delta_{i-1}+\Delta_{i+1}}2-B_i \]

可以转为递推式求解。

变为 \(B_i=0\) 的情况后,观察 \(\frac{E'_{i-1}+E'_{i+1}}2\) 很像琴生不等式,考虑求凸包,故对 \(i\in [0,n]\cap\mathbb{N}\),放置点 \((i,A_i-\Delta_i)\),求出上凸包 \(f(x)\),不难证明 \(E'_i=f(i)\) 是合法解(唯一性我也不会证,感受一下,原方程作为有实际意义的东西,应该是有唯一解)。

???+ 解的唯一性证明(from gemini)

数学证明:随机游走最优停时解的唯一性与凸包性质

1. 证明解 \(E\) 必须是“凹极大值” (Concave Majorant)

首先证明任何满足上述方程的解 \(E\),在几何形态上必须“盖住” \(w\) 且形状是“上凸”的。

1.1 极大值性质 (Majorant Property)
由方程定义 \(E_i = \max(w_i, \dots)\) 可知,显然有:

\[E_i \ge w_i \]

这说明函数 \(E\) 的图像位于 \(w\) 的上方。

1.2 凹性质 (Concavity/Superharmonicity)
由方程 \(E_i = \max(\dots, \frac{E_{i-1} + E_{i+1}}{2})\) 可知:

\[E_i \ge \frac{E_{i-1} + E_{i+1}}{2} \iff 2E_i \ge E_{i-1} + E_{i+1} \]

这正是离散函数的上凸定义(或称为超调和函数)。

结论 A:任何合法解 \(E\) 都是 \(w\) 的一个凹极大值。


2. 证明解 \(E\) 的唯一性与最小性 (\(E = H\))

设 \(H\) 为 \(w\) 的上凸包。数学上,\(H\) 定义为所有大于等于 \(w\) 的上凸函数中最小的一个。
我们需要证明 \(E \equiv H\)。

步骤 2.1:证明 \(E \ge H\)

由于我们已经证明了 \(E\) 是一个“凹极大值”(结论 A),而 \(H\) 是最小的凹极大值。根据定义的直接推论:

\[\forall i, \quad E_i \ge H_i \]

步骤 2.2:证明 \(E \le H\) (关键步骤)

我们需要证明 \(E\) 不可能比 \(H\) 更高。定义差值函数:

\[\Delta_i = E_i - H_i \]

已知 \(\Delta_i \ge 0\),且在边界处 \(\Delta_0 = \Delta_{n+1} = 0\)。我们需要证明 \(\Delta_i \equiv 0\)。

我们分析 \(H\) 的结构,将点集分为两类:

  1. 悬空段 (Linear Segments):即 \(H_i > w_i\) 的区域。

    • 根据凸包性质,此处 \(H\) 是线性的(橡皮筋绷直):\(2H_i = H_{i-1} + H_{i+1}\)。
    • 因为 \(E_i \ge H_i > w_i\),即 \(E_i > w_i\)。根据贝尔曼方程,最优策略是“继续游走”,所以 \(E\) 在此处也是线性的:\(2E_i = E_{i-1} + E_{i+1}\)。
    • 推论:差值 \(\Delta_i\) 在此处是线性的(调和的)。
  2. 接触点 (Contact Points):即 \(H_i = w_i\) 的区域。

    • 假设在某点 \(E_i > H_i\)。则意味着 \(E_i > w_i\)。
    • 一旦 \(E_i > w_i\),根据方程,最优策略依然是“继续游走”,\(E\) 必须保持线性。
    • 推论:如果 \(\Delta_i > 0\),则 \(E\) 必须线性传递这个“高出的部分”。

利用最大值原理 (Maximum Principle):
考虑差值函数 \(\Delta\)。在所有 \(\Delta_i > 0\) 的区域(即 \(E\) 比 \(H\) 高出的区域),\(E\) 和 \(H\) 必然都是线性的(因为都没有选择停止)。
这意味着 \(\Delta\) 也是线性的(\(2\Delta_i = \Delta_{i-1} + \Delta_{i+1}\))。

一个线性的折线函数,如果中间有正值(\(\Delta_i > 0\)),那么它的最大值一定不能在内部取得,只能在边界延伸。
然而,我们已知边界条件被“钉死”在 0:

\[\Delta_0 = 0, \quad \Delta_{n+1} = 0 \]

一条两头为 0 的线段,中间不可能鼓起来。

\[\therefore \forall i, \Delta_i = 0 \implies E_i = H_i \]


3. 物理直观总结

  • \(w\) (木桩):地面上高低不平的障碍物。
  • \(H\) (凸包/橡皮筋):覆盖在木桩上的一根紧绷的橡皮筋。它靠木桩支撑,在无支撑处呈直线。
  • \(E\) (解):
    • \(E \ge H\):解必须盖住木桩,且不能向下凹陷,所以至少要像橡皮筋一样高。
    • \(E \le H\):解不能比橡皮筋更高。因为如果要比橡皮筋高,就必须有额外的“支撑力”(即 \(E_i = w_i\) 的停止收益)。但在橡皮筋悬空的地方,已经没有木桩能顶到更高了,所以解不可能凭空悬浮。

最终结论:方程的解唯一,且正是 \(w\) 的上凸包。

???-

???+ 一些(可能比较唐)的思考+细节

  1. 为什么 \(B_i=0\) 需要求凸包而不是直接取最大值?

环上确实可以直接取最大值,但是我们是先断环成链,再转为 \(B_i=0\),此时不到最大值就一直走有走出边界的风险。

  1. 凸包的边界

凸包的边界应恰为 \(0\) 和 \(n\),因为二者(尤其是 \(n\))在转为 \(B_i=0\) 后不再满足方程条件,不能带上零点求凸包。这也是为什么看上去转化后有可能 \(A'_{n-1}>2A'_{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;
typedef __int128_t int128;
typedef long double ld;
template<typename T>
void chkmax(T &x,const T &y){x=max(x,y);}
template<typename T>
void chkmin(T &x,const T &y){x=min(x,y);}
const int N=200005;
int n;
ll a[N<<1],b[N<<1],d[N];
int main(){
    scanf("%d",&n);
    for(int i=1;i<=n;i++) scanf("%lld",&a[i]),a[n+i]=a[i];
    for(int i=1;i<=n;i++) scanf("%lld",&b[i]),b[n+i]=b[i];
    int m=0;
    for(int i=1;i<=n;i++) if(a[i]>a[m]) m=i;
    for(int i=1;i<=n;i++) a[i]=a[i+m],b[i]=b[i+m];
    a[0]=a[n],b[0]=b[n];
    for(int i=1;i<n;i++) d[i+1]=2*(d[i]+b[i])-d[i-1];
    vector<pil> vec;
    for(int i=0;i<=n;i++){
        pil p{i,a[i]-d[i]};
        while(vec.size()>=2){
            pil q=vec.back(),r=vec[vec.size()-2];
            if((int128)(p.second-q.second)*(q.first-r.first)>=(int128)(q.second-r.second)*(p.first-q.first)) vec.pop_back();
            else break;
        }
        vec.push_back(p);
    }
    int128 sum=0;
    for(int i=1;i<vec.size();i++){
        pil p=vec[i-1],q=vec[i];
        sum+=(int128)(p.second+q.second)*(q.first-p.first+1)-2*p.second;
    }
    for(int i=1;i<=n;i++) sum+=2*d[i];
    printf("%.12Lf\n",(ld)sum/2/n);
    return 0;
}

P6628 [省选联考 2020 B 卷] 丁香之路

想到了正解做法,不会证明。

同类题:OIFC 260123 T3,P6168

有这种必经边的问题,考虑建图,先把必经边连上,问题变为连边权和尽量小的边,使得存在 \(s\) 到 \(t\) 路径遍历所有边,即欧拉路径。

先额外连上 \((s,t)\),把欧拉路径变为欧拉回路。

考虑欧拉回路的充要条件:

  1. 所有点度数均为偶数,

  2. 非孤立点连通。

那就对这两个分别做。

先把所有奇度点拉出来,相邻的点连边(\(u,v\) 连边为在图中加入 \((u,u+1),(u+1,u+2),\dots (v-1,v)\))。

再把所有非孤立点拉出来,跑 Kruskal,每条边有 \(2\) 倍贡献,这里只需要把连接相邻非孤立点的边排序即可。

???+ 证明

学习自本篇。

只需证明所有解都可以看成先在不允许加入重边的情况下保证度数,再连通,再证明上述过程在这两步均是最优即可。

对于一个解,我们不断去重边,每次去掉的两条重边都认为是第二步加入的,去重边显然不改变度数奇偶性,故每个解都被拆成了上述两步。

对于第一步,由于连边只能连接相邻两个点(否则可以拆),第 \(1\) 个奇度点只能连第 \(2\) 个,第 \(3\) 个只能连第 \(4\) 个……以此类推,可以证明第一步的构造唯一。

对于第二步,由于是 MST,一定最优。

???-

#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=2505;
int n,m,s,deg[N],fa[N],deg0[N],fa0[N];
ll ans=0,ans0;
vector<pii> es,es2;
int find(int x){
    if(fa[x]==x) return x;
    return fa[x]=find(fa[x]);
}
void merge(int x,int y){fa[x]=y;}
void init(){
    for(int i=1;i<=n;i++) deg[i]=0,fa[i]=i;
    ans=0;
    for(auto [u,v]:es) deg[u]++,deg[v]++,merge(find(u),find(v)),ans+=abs(u-v);
}
int main(){
    scanf("%d%d%d",&n,&m,&s);
    for(int i=1;i<=m;i++){
        int u,v;
        scanf("%d%d",&u,&v);
        es.push_back({u,v});
    }
    init();
    for(int t=1;t<=n;t++){
        ans0=ans;
        for(int i=1;i<=n;i++) deg0[i]=deg[i],fa0[i]=fa[i];
        deg[s]++,deg[t]++,merge(find(s),find(t));
        // printf("%lld\n",ans);
        int lst=0;
        for(int i=1;i<=n;i++){
            if(lst) deg[lst]++,deg[i]++,ans++,merge(find(lst),find(i));
            if(deg[i]&1) lst=i;
            else lst=0;
        }
        // printf("%lld\n",ans);
        es2.clear();
        lst=0;
        for(int i=1;i<=n;i++){
            if(!deg[i]) continue;
            if(lst) es2.push_back({lst,i});
            lst=i;
        }
        sort(es2.begin(),es2.end(),[&](pii x,pii y){return abs(x.first-x.second)<abs(y.first-y.second);});
        for(auto [u,v]:es2){
            if(find(u)==find(v)) continue;
            ans+=2*abs(u-v);
            merge(find(u),find(v));
        }
        printf("%lld ",ans);
        // printf("\n---\n");
        ans=ans0;
        for(int i=1;i<=n;i++) deg[i]=deg0[i],fa[i]=fa0[i];
    }
    printf("\n");
    return 0;
}

P6168 [IOI 2016] railroad

与上一道题(丁香之路)类似,由于这两个一起做,本题想一会就会了,不过独立做可能还是会卡(比如去想 Hamilton 回路之类)。

做法类似,还是先保证度数平衡,再跑 MST,证明也是发现所有解都可以拆成这两步,并且这两步都已经最优。

记得离散化。

值得注意的是,有向图欧拉回路“连通性”的定义为:视为无向图后连通。

???+ 解释

在保证度数平衡后,只需要在无向图意义下连通即可保证原图强连通,进而保证有欧拉回路。

反证法,在上述条件下,若图没有强连通性,则缩点,考虑零出度 scc,其整体的入度出度显然是不平衡的,故内部的点入度出度不可能都平衡,矛盾。

???-

#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=400005;
int n,tot,s[N],t[N],deg[N],fa[N];
vector<int> num;
unordered_map<int,int> mp;
int find(int x){
    if(fa[x]==x) return x;
    return fa[x]=find(fa[x]);
}
void merge(int x,int y){fa[x]=y;}
int main(){
    scanf("%d%*d",&n);
    num.push_back(1e9+5);
    for(int i=1;i<=n;i++){
        scanf("%d%d",&s[i],&t[i]);
        s[i]++,t[i]++;
        num.push_back(s[i]),num.push_back(t[i]);
    }
    sort(num.begin(),num.end());
    num.erase(unique(num.begin(),num.end()),num.end());
    tot=num.size();
    for(int i=0;i<tot;i++) mp[num[i]]=i+1;
    for(int i=1;i<=tot;i++) fa[i]=i;
    for(int i=1;i<=n;i++){
        s[i]=mp[s[i]],t[i]=mp[t[i]];
        deg[s[i]]++,deg[t[i]]--;
        merge(find(s[i]),find(t[i]));
    }
    deg[tot]++,deg[1]--;
    // for(int i=1;i<=tot;i++) printf("%d ",deg[i]);printf("\n");
    ll ans=0;
    for(int i=1;i<tot;i++){
        if(deg[i]) merge(find(i),find(i+1));
        if(deg[i]>0) ans+=(ll)deg[i]*(num[i]-num[i-1]);
        deg[i+1]+=deg[i],deg[i]=0;
    }
    // for(int i=1;i<=tot;i++) printf("%d ",find(i));printf("\n");
    // printf("%lld\n",ans);
    vector<pii> es;
    for(int i=2;i<=tot;i++) es.push_back({i-1,i});
    sort(es.begin(),es.end(),[&](pii x,pii y){return num[x.second-1]-num[x.first-1]<num[y.second-1]-num[y.first-1];});
    for(auto [u,v]:es){
        if(find(u)==find(v)) continue;
        ans+=num[v-1]-num[u-1];
        merge(find(u),find(v));
    }
    printf("%lld\n",ans);
    return 0;
}

*P11983 [JOIST 2025] 展览会 3 / Exhibition 3

很复杂的一道题,鉴于笔者完全没做出来,直接贴一篇题解好了。

记录一些 trick:

  1. 限制二分范围的方法:先倍增,再二分。如果二分 \(\text{check}\) 的复杂度是 \(\f(mid)\),答案范围是 \([1,n]\),可以先从小到大倍增找到最大满足 \(2^k\le ans\) 的 \(k\),再在 \([2^k,2^{k+1})\) 内二分,复杂度从 \(\mathcal{O}(f(n)\log n)\) 降至 \(\mathcal{O}(f(ans)\log ans)\)。对于需要 \(m\) 次二分的情况,复杂度从 \(\mathcal{O}(mf(n)\log n)\) 降至 \(\mathcal{O}(\sum f(ans)\log n)\)。
  2. 最少点覆盖区间问题,加入一个区间判断答案是否变大。正着反着各跑一遍,则求出了每个点最靠右/左位置(若能更靠右,则左侧必有区间不被覆盖),且每个点可以取到的位置连续(钦定一个点位置,左边都取最靠右,右边都取最靠左)。记每个点合法位置为 \([fl,fr]\),加入的区间 \([L,R]\) 不会使答案变大当且仅当存在 \(i\) 使得 \([fl_i,fr_i]\cap[L,R]\neq\varnothing\)。

P8078 [WC2022] 秃子酋长

法一:经典做法——不插入莫队

直接考虑莫队,用 set 维护前驱后继,插入时间为 \(\mathcal{O}(\log)\),复杂度 \(\mathcal{O}(n\sqrt{n}\log n)\) 或 \(\mathcal{O}(n\sqrt{n\log n})\)(认为 \(n,q\) 同阶,下同)。

注意到若同时用链表维护,则删除时间可做到 \(\mathcal{O}(1)\),想到类似于回滚莫队的东西,考虑反过来做,同一块内按 \(r\) 从大到小排序,每做完一个查询把 \(l\) 回滚到块的左端点,换块时回滚右端点到 \(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=500005,B=707;
int n,m,pre[N],nxt[N];
pii a[N];
ll ans[N],sum;
vector<pil> st;
struct Query{
    int l,r,id;
};
Query qs[N];
int kid(int i){
    return (i-1)/B+1;
}
bool cmp(Query x,Query y){
    int a=kid(x.l),b=kid(y.l);
    if(a!=b) return a<b;
    return x.r>y.r;
}
void remove(int i,bool flag=false){
    // printf("remove %d pre %d nxt %d sum %lld\n",i,pre[i],nxt[i],sum);
    if(flag) st.push_back({i,sum});
    if(pre[i]) sum-=abs(i-pre[i]);
    if(nxt[i]) sum-=abs(i-nxt[i]);
    pre[nxt[i]]=pre[i],nxt[pre[i]]=nxt[i];
    if(pre[i]&&nxt[i]) sum+=abs(pre[i]-nxt[i]);
    // printf("sum=%lld\n",sum);
}
void undo(){
    auto [i,s]=st.back();
    nxt[pre[i]]=i,pre[nxt[i]]=i,sum=s;
    st.pop_back();
}
int main(){
    scanf("%d%d",&n,&m);
    for(int i=1;i<=n;i++) scanf("%d",&a[i].first),a[i].second=i;
    sort(a+1,a+n+1);
    int l=1,r=n,L;
    nxt[0]=a[1].second,pre[0]=a[n].second;
    pre[a[1].second]=nxt[a[n].second]=0;
    for(int i=1;i<n;i++) nxt[a[i].second]=a[i+1].second,pre[a[i+1].second]=a[i].second,sum+=abs(a[i].second-a[i+1].second);
    for(int i=1;i<=m;i++) scanf("%d%d",&qs[i].l,&qs[i].r),qs[i].id=i;
    sort(qs+1,qs+m+1,cmp);
    for(int i=1;i<=m;i++){
        if(i==1||kid(qs[i].l)!=kid(qs[i-1].l)){
            L=(kid(qs[i].l)-1)*B+1;
            while(r<n) undo(),r++;
            while(l<L) remove(l++);
        }
        while(r>qs[i].r) remove(r--,1);
        while(l<qs[i].l) remove(l++,1);
        ans[qs[i].id]=sum;
        while(l>L) undo(),l--;
    }
    for(int i=1;i<=m;i++) printf("%lld\n",ans[i]);
    return 0;
}

*做法二:猫树分治

学习自本篇。

认为排序后相邻两个数的贡献是从一个位置移动到另一个位置的步数。

考虑分治,到 \([L,R]\) 时,处理所有跨 \(mid=\frac{L+R}2\) 的询问,左右贡献分开计算(在左侧的移动认为是左侧贡献,另一侧同理)。

不妨只考虑右侧,对于排序后相邻的两个位置 \(i,j\),其贡献有两种:

  1. 左侧 \(\exists k,a_k\in [a_i,a_j]\),则贡献为 \(i+j-2mid\),
  2. 其他情况,贡献为 \(|i-j|\)。

只需判定左侧一个后缀里是否有在指定范围中的数,可以离线二维数点,用 BIT 维护(或者离散化,预处理每个数出现位置的 ST 表,在线查询,常数不知道如何,笔者乱胡的)。

正着加常数有点大,用类似做法一的方法,反着删除,用链表维护即可。

P6773 [NOI2020] 命运

不难想到 DP,设 \(f_{u,d}\) 表示考虑了 \(u\) 子树,最深的限制深度为 \(d\)(子树中所有限制都满足当且仅当最深的限制满足),转移考虑 \((u,v)\) 这条边有没有被选。考虑转移,“子树”其实是不明确的,为了方便,我们认为 \((u,fa_u)\) 在 \(u\) 处被决策,这样每个点只会做一次决策。

初始状态:

\[f_{u,lim_u}=1 \]

其中 \(lim_u\) 为 \(u\) 上所有限制最深的深度。

加入儿子 \(v\) 时转移方程:

\[f'_{u,\max(d_1,d_2)}\leftarrow f_{u,d_1}f_{v,d_2} \]

所有儿子加完后再决策 \((u,fa_u)\):

\[f_{u,0}\leftarrow f_{u,d} (d\in [0,dep_u)) \]

注意到核心转移为 \(\max\) 卷积,考虑线段树合并,维护区间和即可。

???+ 线段树合并维护 \(\max\) 卷积

在递归过程中,维护 \(s_1,s_2\) 分别表示两棵树当结点之前的前缀和,合并时打区间乘标记,具体地:

  1. 有一个结点非空,用另一侧的 \(s\) 区间乘非空一侧;
  2. 两个叶子,用 \(s\) 和结点上的数交叉相乘,再加上两个结点相乘;
  3. 两个结点均非空,递归处理。

注意到区间乘标记 \(\text{pushdown}\) 不会新增结点,所以复杂度是对的。

???-

#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=500005;
int n,m,dep[N],fa[N],lim[N];
vector<int> G[N];
void dfs1(int u,int f){
    fa[u]=f,dep[u]=dep[f]+1;
    for(auto v:G[u]){
        if(v==f) continue;
        dfs1(v,u);
    }
}
int tot=0,rt[N];
struct Node{
    int lc,rc;
    int sum,lazy;
    Node(){lc=rc=sum=0,lazy=1;}
};
Node tr[N<<6];
void pushup(int x){
    add(tr[x].sum=tr[tr[x].lc].sum,tr[tr[x].rc].sum);
}
void _modify(int x,int v){
    if(!x) return;
    tr[x].sum=(ll)tr[x].sum*v%MOD,tr[x].lazy=(ll)tr[x].lazy*v%MOD;
}
void pushdown(int x){
    _modify(tr[x].lc,tr[x].lazy),_modify(tr[x].rc,tr[x].lazy);
    tr[x].lazy=1;
}
void add(int &x,int l,int r,int p,int v){
    if(!x) x=++tot;
    if(l==r){
        add(tr[x].sum,v);
        return;
    }
    int mid=(l+r)>>1;
    pushdown(x);
    if(p<=mid) add(tr[x].lc,l,mid,p,v);
    if(p>mid) add(tr[x].rc,mid+1,r,p,v);
    pushup(x);
}
void merge(int &x,int y,int l,int r,int s1,int s2){
    if(!x||!y){
        // printf("merge %d %d %d %d %d %d (%d,%d) (%d,%d)\n",x,y,l,r,s1,s2,tr[x].sum,tr[x].lazy,tr[y].sum,tr[y].lazy);
        if(!x&&!y) return;
        if(!x) x=y,_modify(y,s1);
        if(!y) _modify(x,s2);
        // printf("%d (%d,%d)\n",x,tr[x].sum,tr[x].lazy);
        return;
    }
    if(l==r){
        tr[x].sum=((ll)tr[x].sum*s2+(ll)tr[y].sum*s1+(ll)tr[x].sum*tr[y].sum)%MOD;
        return;
    }
    int mid=(l+r)>>1;
    pushdown(x),pushdown(y);
    int os1=s1,os2=s2;
    add(s1,tr[tr[x].lc].sum),add(s2,tr[tr[y].lc].sum);
    merge(tr[x].lc,tr[y].lc,l,mid,os1,os2);
    merge(tr[x].rc,tr[y].rc,mid+1,r,s1,s2);
    pushup(x);
}
int query(int x,int l,int r,int ql,int qr){
    if(!x||(ql<=l&&r<=qr)) return tr[x].sum;
    int mid=(l+r)>>1,s=0;
    pushdown(x);
    if(ql<=mid) add(s,query(tr[x].lc,l,mid,ql,qr));
    if(qr>mid) add(s,query(tr[x].rc,mid+1,r,ql,qr));
    return s;
}
void dfs2(int u){
    add(rt[u],0,n,lim[u],1);
    for(auto v:G[u]){
        if(v==fa[u]) continue;
        dfs2(v);
        merge(rt[u],rt[v],0,n,0,0);
    }
    if(u!=1) add(rt[u],0,n,0,query(rt[u],0,n,0,dep[u]-1));
}
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,1);
    scanf("%d",&m);
    for(int i=1;i<=m;i++){
        int u,v;
        scanf("%d%d",&u,&v);
        chkmax(lim[v],dep[u]);
    }
    dfs2(1);
    printf("%d\n",query(rt[1],0,n,0,0));
    return 0;
}

*P7275 计树

新学到一个容斥 trick。

???+ ver1

非容斥部分比较简单,称 \(|u-v|=1\) 的边为黑边,反之为白边,发现黑边一定能构成一对连通块,这些连通块大小 \(\ge 2\),且把 \(1\sim n\) 分为了若干段。对白边应用 Cayley 公式,若连通块大小依次为 \(s_1,s_2,\dots,s_k\),则生成树数量为 \(n^{k-2}\prod\limits_{i=1}^ks_k\)。对于固定的 \(k\),上一个式子求和可写为生成函数形式 \(n^{-2}[x^n]F^k(x)\),其中 \(F(x)=2nx^2+3nx^3+\dots\)。

注意到这样会算重,有一些白边可能实际上是黑边,用这类容斥,设 \(G(x)\) 为容斥系数,则 \(G(x)=\frac{x^2}{1-x+x^2}\)。再将 \(F(x)\) 与 \(G(x)\) 对位乘,可得 \(n\vartheta G(x)=nx\frac{d}{dx}G(x)=-\frac{nx^2(x-2)}{(x^2-x+1)^2}\),答案即 \(n^{-2}[x^n]\frac{1}{1-nx\frac{d}{dx}G(x)}=n^{-2}[x^n]\frac{(x^2-x+1)^2}{(x^2-x+1)^2+nx^2(x-2)}\),用 Bostan-Mori 可做到 \(\mathcal{O}(\log n)\)。

???-

非容斥部分比较简单,称 \(|u-v|=1\) 的边为黑边,反之为白边,发现黑边一定能构成一堆连通块,这些连通块大小 \(\ge 2\),且把 \(1\sim n\) 分为了若干段。Cayley 公式,若连通块大小依次为 \(s_1,s_2,\dots,s_k\),则树数量为 \(n^{k-2}\prod\limits_{i=1}^ks_k\)。对于固定的 \(k\),上一个式子求和可写为生成函数形式 \(n^{-2}[x^n]F^k(x)\),其中 \(F(x)=2nx^2+3nx^3+\dots\)。

注意到这样会算重,有一些白边可能实际上是黑边,每一段在它的所有划分处都会产生贡献,而我们要求每一段在长度 \(\ge 2\) 时总贡献 \(1\) 次。设 \(G(x)\) 为容斥系数,表示贡献的倍率,有方程 \(\frac{1}{1-G(x)}-1=\frac{x^2}{1-x}\),解得 \(G(x)=\frac{x^2}{1-x+x^2}\)。

再将 \(F(x)\) 与 \(G(x)\) 对位乘,可得 \(H(x)=n\vartheta G(x)=nx\frac{d}{dx}G(x)=-\frac{nx^2(x-2)}{(x^2-x+1)^2}\),答案即 \(n^{-2}[x^n]\frac{1}{1-H(x)}=n^{-2}[x^n]\frac{(x^2-x+1)^2}{(x^2-x+1)^2+nx^2(x-2)}\),用 Bostan-Mori 可做到 \(\mathcal{O}(\log 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 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;
}
void split(const vector<int> &f,vector<int> &f0,vector<int> &f1){
    f0.clear(),f1.clear();
    for(int i=0;i<f.size();i++){
        if(i&1) f1.push_back(f[i]);
        else f0.push_back(f[i]);
    }
}
vector<int> mul(const vector<int> &f,const vector<int> &g){
    vector<int> tmp(f.size()+g.size()-1);
    for(int i=0;i<f.size();i++) for(int j=0;j<g.size();j++) add(tmp[i+j],(ll)f[i]*g[j]%MOD);
    return tmp;
}
vector<int> add(vector<int> f,const vector<int> &g){
    if(f.size()<g.size()) f.resize(g.size());
    for(int i=0;i<g.size();i++) add(f[i],g[i]);
    return f;
}
vector<int> sub(vector<int> f,const vector<int> &g){
    if(f.size()<g.size()) f.resize(g.size());
    for(int i=0;i<g.size();i++) add(f[i],MOD-g[i]);
    return f;
}
vector<int> shift(vector<int> f){
    f.push_back(0);
    for(int i=f.size()-1;i>0;i--) f[i]=f[i-1];
    f[0]=0;
    return f;
}
int BM(vector<int> A,vector<int> B,int k){
    if(k==0) return (ll)A[0]*qpow(B[0],MOD-2)%MOD;
    vector<int> A0,A1,B0,B1;
    split(A,A0,A1),split(B,B0,B1);
    if(k&1) return BM(sub(mul(A1,B0),mul(A0,B1)),sub(mul(B0,B0),shift(mul(B1,B1))),k>>1);
    return BM(sub(mul(A0,B0),shift(mul(A1,B1))),sub(mul(B0,B0),shift(mul(B1,B1))),k>>1);
}
int n;
int main(){
    scanf("%d",&n);
    vector<int> A{1,MOD-1,1},B{0,0,MOD-(n<<1),n};
    A=mul(A,A);
    printf("%lld\n",(ll)BM(A,add(A,B),n)*qpow(n,MOD-3)%MOD);
    return 0;
}

*ARC134F Flipping Coins

与上一道题类似,笔者不但转化歪了,而且没容斥出来。

置换问题,而且还是 \(i\) 与 \(p_i\) 操作,想到置换环,于是对一个单独的环考虑,最后 \(exp\) 即可(事实上这里没有必要,不过由于看着比较自然,遂保留)。

考虑一个点 \(i\) 有贡献的条件,显然要 \(i,p^{-1}_i\) 先后操作一次,于是要求 \(i<p^{-1}_i\) 且 \(p^{-1}_i\) 成功操作。而 \(p^{-1}_i\) 成功操作要求 \(p^{-1}_i<p^{-2}_i\) 或 \(p^{-2}_i\) 不能成功操作。\(p^{-2}_i\) 不能成功操作要求 \(p^{-2}_i>p^{-3}_i\) 且 \(p^{-3}_i\) 成功操作……

不断进行下去,发现对于一个极长递增段,只有点数为奇数时会在下一位产生贡献(然后笔者脑抽又讨论了递减段,然后不会了),所以用 EGF 把递增段组合。

注意到直接组合可能会不合法,因为要求极长,所以用上一道题(P7275)的套路。设 \(F(x)=\sum\limits_{i=1}^{\infty}w^{2\nmid i}x^i\) 表示目标贡献,\(G(x)\) 表示带上容斥系数后的贡献,则有方程 \(\frac{1}{1-G(x)}-1=F(x)\),解得 \(G(x)=1-\frac{1}{1+F(x)}\),将 OGF \(G(x)\) 转为 EGF \(\widehat{G}(x)\)。

对于单个环,其由若干递增段构成,\(i\) 个递增段排列方案数为 \((i-1)!\),集合划分需要除 \(i!\),故单个环的 EGF 为

\[\sum_{i=1}^{\infty}\frac{(i-1)!\widehat{G}^i(x)}{i!}=\sum_{i=1}^{\infty}\frac{\widehat{G}^i(x)}{i}=-\ln(1-\widehat{G}(x)) \]

答案为

\[\left[\frac{x^n}{n!}\right]exp(-ln(1-\widehat{G}(x)))=\left[\frac{x^n}{n!}\right]\frac{1}{1-\widehat{G}(x)} \]

*CF2196D Double Bracket Sequence

做法 1:直接贪心

场上差点想出来了

考虑单种括号的版本,有一个看起来很对的贪心是,从左往右扫,当加入一个不匹配的右括号时,改为左括号,一直进行下去,最后再把剩下的左括号中的一半改为右括号即可。

对于两种括号的版本,还是像刚才这样贪心,遇到一个不匹配的右括号,此时会有三种改法:以 ) 为例,可以改为 (,[ 和 ](最后一种有可能不合法)。直接暴力维护这三种状态,继续扫描,如果有些状态不需要修改,而有些需要,只保留不需要修改的状态(这里场上没想出来),否则和刚才一样。可以证明,如果需要修改,则只有一个状态是合法的(因为每次保留的都是失配左括号较多部分的,到这种情况则取到失配左括号数最多的情况,这个只有一种,比如 ( 改为 ))。最后统计答案即刚才扫描过程中的修改次数 \(+\min\{\frac{x+y}2\}\)(\(x,y\) 是两种括号失配左括号数量,也即把括号看为 \(\pm1\) 后的总和)。

证明需要依赖做法 2,可以证明这种做法得到的构造总有一个与做法 2 本质相同。

#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;
char s[N];
int type(char c){
    if(c=='('||c==')') return 0;
    return 1;
}
void __INIT__(){}
void __SOLVE__(){
    scanf("%d%s",&n,s+1);
    vector<array<int,2>> v{{0,0}};
    int ans=0,mn=inf;
    for(int i=1;i<=n;i++){
        int t=type(s[i]);
        vector<array<int,2>> nv;
        for(auto sum:v){
            if(s[i]=='('||s[i]=='[') sum[t]++,nv.push_back(sum);
            else if(sum[t]) sum[t]--,nv.push_back(sum);
        }
        if(nv.size()){
            v.swap(nv);
            continue;
        }
        ans++;
        for(auto sum:v){
            sum[t]++,nv.push_back(sum),sum[t]--;
            sum[t^1]++,nv.push_back(sum),sum[t^1]--;
            if(sum[t^1]) sum[t^1]--,nv.push_back(sum);
        }
        v.swap(nv);
    }
    for(auto sum:v) chkmin(mn,(sum[0]+sum[1])>>1);
    printf("%d\n",ans+mn);
}
int main(){
    int T=1;
    scanf("%d",&T);
    __INIT__();
    while(T--) __SOLVE__();
    return 0;
}

*做法 2

对每对匹配单独考虑,不要求匹配不交叉,因为交叉可以调整为包含。每对匹配需要花费 \(0/1/2\) 次操作(后称为 \(0/1/2\) 类匹配)。对于一个长 \(n\) 的括号序列,只用 \(1/2\) 类匹配则操作次数为 \(\frac{n}2\sim\frac{n}2+1\),去掉一个 \(0\) 类匹配后,操作次数为 \(\frac{n}2-1\sim\frac{n}2\),一定不劣。所以先最大化 \(0\) 次操作的匹配数量,即对两种括号分别求最长匹配子序列。发现 \(2\) 类匹配最多有 \(1\) 个(key observation),因为可以调整,考虑其出现的充要条件,发现是对于去掉 LPS 后剩下的序列 \(s'\),存在一个分界点 \(x\),使得 \(x\) 前面仅由 ),] 构成,后面仅由 (,[ 构成,且左、右括号数量均为奇数,例:))]([[。

#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,cnt,lst;
char s[N];
vector<int> stk[2];
pii type(char c){
    if(c=='(') return {0,1};
    if(c==')') return {0,-1};
    if(c=='[') return {1,1};
    return {1,-1};
}
void __INIT__(){}
void __SOLVE__(){
    scanf("%d%s",&n,s+1);
    cnt=0;
    stk[0].clear(),stk[1].clear();
    for(int i=1;i<=n;i++){
        auto [t,d]=type(s[i]);
        if((int)stk[t].size()+d>=0){
            if(d==1) stk[t].push_back(i);
            else stk[t].pop_back();
        }
        else cnt++,lst=i;
    }
    int sum=stk[0].size()+stk[1].size(),fir=n+1;
    if(stk[0].size()) chkmin(fir,stk[0][0]);
    if(stk[1].size()) chkmin(fir,stk[1][0]);
    int ans=(cnt+sum)>>1;
    if((cnt&1)&&(sum&1)&&lst<fir) ans++;
    printf("%d\n",ans);
}
int main(){
    int T=1;
    scanf("%d",&T);
    __INIT__();
    while(T--) __SOLVE__();
    return 0;
}

*P4118 [Ynoi2018] 末日时在做什么?有没有空?可以来拯救吗?

还可以的分块题,又忘了等比数列求和……

这种题看着就很维护凸包,考虑用什么结构分段维护,想到分块。对每块维护一个凸包,横坐标表示区间长度,纵坐标表示块内该长度的区间和的最大值。整块修改、整块查询、散块查询分别容易做到 \(\mathcal{O}(1),\mathcal{O}(\log B),\mathcal{O}(B)\),首要问题是散块修改怎么做。

通过一次扫描完成重构显然不现实,考虑分治,即建立线段树的结构。每个结点维护前缀、后缀、子段和的凸包,合并时做闵可夫斯基和即可,可以做到 \(\mathcal{O}(B\log B)\)。这太慢了,考虑优化,注意到实际上需要重构的结点数很少,每层只有 \(\mathcal{O}(1)\) 个结点需要重构(然后笔者以为这没用,忘了有个东西叫等比数列求和),并且重构的复杂度为等比数列,故这些点重构的总复杂度为 \(\mathcal{O}(B)\),给结点打加法标记的复杂度 \(\mathcal{O}(1)\)。综上,散块修改复杂度 \(\mathcal{O}(B)\)。

现在做到了 \(\mathcal{O}(n\sqrt{n\log n})\),但仍能优化(后面就完全不是笔者自己想到的了)。注意到每一块独立,尝试离线下来,把整块查询变为指针扫描而不是二分,直接离线下来,把两次重构之间的查询按全局加标记从小到大排序,每次直接暴力往后扫指针即可,使用势能分析复杂度:令势能为指针到该块右端点距离总和,则势能增加量为 \(\mathcal{O}(mB)\),每减小 \(1\) 需要 \(\mathcal{O}(1)\) 时间,故总复杂度 \(\mathcal{O}(mB)\)。

现在瓶颈在把整块查询排序,这个改为基数排序即可,最终复杂度 \(\mathcal{O}(m\sqrt{n})\)。

代码据说很长,而且卡常,不想写。

P5073 [Ynoi Easy Round 2015] 世上最幸福的女孩

建线段树,每个结点维护凸包,先不直接查询,把每个结点上的查询按二分的斜率排序,用一个指针扫描。总复杂度 \(\mathcal{O}((n+m)\log n)\)。

P3634 [APIO2012] 守卫

笔者做法还是有点烂……

一开始忘了还有 \(0\) 的限制,想起来后发现这个也不难,用链表/并查集/差分等删除不可选的点(即被 \(c=0\) 区间覆盖的点)即可。

由于做了 P11983,立刻想到 \(k\) 为合法最小值的情况,对于一般情况,逐个点考虑。由于假设了点可被重复放置,所以需要先判掉 \(k=n'\)(\(n'\) 是去掉不可选的点后的点数)。对于一个点 \(x\),考虑正难则反,如果其必须不能选,则剩余位置覆盖所有区间所需点数 \(\le k\),预处理正着和反着的贪心,\(x\) 前面用正着的贪心,\(x\) 后面用反着的贪心,这样肯定不劣,接下来会剩一些区间,这些区间都包含 \(x\),分讨一下:

  1. 存在区间仅包含 \(x\)(即存在区间 \([x,x]\)):\(x\) 必须选;
  2. 同时存在区间 \([*,x],[x,*]\):所需点数 \(+2\);
  3. 存在包含 \(x\) 区间:所需点数 \(+1\);
  4. 不存在包含 \(x\) 区间:所需点数不变。

判断一下所需点数,如果 \(>k\) 则 \(x\) 必须选。

#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,k,m,fa[N],vis[N],fir[N],tr[N],pos1[N],pos2[N],locked[N];
vector<pii> vec;
vector<int> L,R;
int find(int x){
    if(fa[x]==x) return x;
    return fa[x]=find(fa[x]);
}
void merge(int x,int y){fa[x]=y,fir[y]=fir[x];}
#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 s=0;
    for(;x;x-=lowbit(x)) s+=tr[x];
    return s;
}
int main(){
    scanf("%d%d%d",&n,&k,&m);
    for(int i=1;i<=n;i++) fa[i]=fir[i]=i,vis[i]=0,pos1[i]=0,pos2[i]=n+1;
    for(int i=1;i<=m;i++){
        int l,r,t;
        scanf("%d%d%d",&l,&r,&t);
        if(t) vec.push_back({l,r});
        else for(int p=find(l);vis[p]=1,p+1<=r;p=find(p)) merge(p,find(p+1));
    }
    for(int i=1;i<n;i++) if(fa[i]==i&&vis[i]&&vis[find(i+1)]) merge(i,find(i+1));
    int cnt=0;
    for(int i=1;i<=n;i++) cnt+=!vis[find(i)];
    if(k==cnt){
        for(int i=1;i<=n;i++) if(!vis[find(i)]) printf("%d\n",i);
        return 0;
    }
    for(auto &[l,r]:vec){
        if(vis[find(l)]) l=find(l)+1;
        if(vis[find(r)]) r=fir[find(r)]-1;
        chkmax(pos1[r],l),chkmin(pos2[l],r);
        if(l==r) locked[l]=1;
    }
    // for(auto [l,r]:vec) printf("[%d,%d] ",l,r);printf("\n");
    sort(vec.begin(),vec.end(),[&](pii x,pii y){return x.first>y.first;});
    R.push_back(n+1);
    for(auto [l,r]:vec){
        if(R.size()&&R.back()<=r) continue;
        R.push_back(l);
    }
    sort(vec.begin(),vec.end(),[&](pii x,pii y){return x.second<y.second;});
    L.push_back(0);
    for(auto [l,r]:vec){
        if(L.size()&&L.back()>=l) continue;
        L.push_back(r);
    }
    // for(auto i:L) printf("%d ",i);printf("\n");
    // for(auto i:R) printf("%d ",i);printf("\n");
    vector<int> ans;
    for(int i=1,p=1,q=R.size()-1,j=0;i<=n;i++){
        if(vis[find(i)]) continue;
        // printf("i=%d\n",i);
        while(p<L.size()&&L[p]<i) p++;
        while(R[q]<=i) q--;
        int cnt=p+q-1,l=L[p-1],r=R[q];
        // printf("cnt %d p %d q %d (%d,%d)\n",cnt,p,q,l,r);
        while(j<vec.size()&&vec[j].second<r) add(vec[j++].first,1);
        if(query(r-1)-query(l)){
            if(pos1[i]>l&&pos2[i]<r) cnt+=2;
            else cnt++;
        }
        if(cnt>k||locked[i]) ans.push_back(i);
    }
    if(ans.empty()) printf("-1\n");
    else for(auto i:ans) printf("%d\n",i);
    return 0;
}

更高妙的做法:

注意到如果 \(k>\) 合法最小值,那么除了被长度为 \(1\) 区间包含的点,其余点都不必选,因为可以去掉这个点,加上相邻的点。对于 \(k\) 为合法最小值,直接正着反着各跑一遍贪心,如果一个点最左侧位置和最右侧位置一致,则位置固定。

*P7213 [JOISC 2020] 最古の遺跡 3

贡献延迟计算神题。

首先不要读错题,一个柱子高度会减小的条件为后面有一个与之高度相同的柱子。

注意到任意后缀,本质不同的高度只变多不变少,即不会有一种高度原来出现过后来没了,证明显然。

接下来发现一个高 \(h\) 的柱子会消失当且仅当其后面不会消失的柱子高度至少遍历 \([1,h]\)。

???+ 证明(可能繁琐了)

首先发现 \(n\) 次地震等价于 \(\infty\) 次地震:一个柱子降到 \(h'\) 的时间(地震次数)不超过 \(n-h'\),考虑归纳,假设对 \(h'\in[k+1,n]\) 均已证明,考虑证明 \(h'=k\),设这个柱子为 \(i\),若 \(i\) 初始高度为 \(h'\) 则显然符合条件,否则设其后面与之相等的柱子为 \(j\),则 \(i,j\) 达到 \(h'+1\) 时间为 \(n-h'-1\),再花 \(1\) 步即可到 \(h'\),用时不超过 \(n-h'\)。

充分性(遍历 \([1,h]\Rightarrow\) 消失):当后缀都达到最终位置后,如果当前柱子没消失,则再过 \(h\) 时间就会消失。

必要性:本质不同的高度只变多不变少,如果一个 \(h_0\in[1,h]\) 一直没有,则当前柱子会卡在 \(h_0\) 处。

???-

接下来一步最神奇,考虑记录终止状态 DP。

设 \(f_{i,j}\) 表示考虑了 \(i\) 的后缀,终止高度最多遍历 \([1,j]\)(\(j+1\) 不在终止高度中),只考虑了终止高度 \([0,j]\) 的柱子(贡献延迟计算)的方案数。用于转移的重要观察:已经考虑的柱子初始高度也在 \([1,j]\) 内,还是因为本质不同的高度只增多。把两个相同高度视为不同,便于转移,最终答案除以 \(2^n\) 即可。

???+ 转移(抄自本篇)

分类讨论一下转移:

  • 如果 \(i\) 钦定消失,那么阈值不变,从 \(f_{i-1,j}\) 转移。

    此时有 \(2j\) 个可用高度,其中有 \(j\) 个分配给了标准柱,还有 \(c_0\) 个已经分配,所以系数为 \(j - c_0\)。

  • 如果 \(i\) 钦定保留,我们同样考虑它的高度:

    • 如果 \(i\) 的高度 \(> j + 1\),我们就稍后考虑它的真实高度,此时从 \(f_{i-1,j}\) 转移,系数为 \(1\)。

    • 如果 \(i\) 的高度为 \(j + 1\),由于有些标准柱的高度还未确定,所以我们需要考虑接下来之后的高度阈值。

      枚举一个新阈值 \(k\),此时是从 \(f_{i-1,j}\) 转移到 \(f_{i,k}\)。

      计算系数,有如下几个部分:

      • 选定标准柱的位置 \(\binom{c_1-j}{k-j-1}\)。
      • 确定当前柱子的长度 \(k - j + 1\),分析方式同第一种转移。
      • 考虑未确定的 \(k - j - 1\) 的形成过程,这里我们用 \(g_{k-j-1}\) 表示。

      因此系数为 \(\binom{c_1-j}{k-j-1} \times g_{k-j-1} \times (k - j + 1)\)。

现在我们考虑 \(g\) 的转移,明确 \(g\) 的含义为 " 将 \(n\) 个石柱震为高度连续的(初始)状态数 "。

其实这个过程很类似于 \(f\) 的第二种转移。我们只需要枚举一下编号最小的柱子的高度:

\[g_n = \sum_{i=1}^n \binom{n-1}{i-1} \times (i+1) \times g_{i-1} \times g_{n-i} \]

???-

求答案,因为终止高度一定遍历 \([1,n]\),所以答案为 \(\frac{f_{1,n}}{2^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 chkmax(T &x,const T &y){x=max(x,y);}
template<typename T>
void chkmin(T &x,const T &y){x=min(x,y);}
const int inf=0x3f3f3f3f;
const ll infll=0x3f3f3f3f3f3f3f3f;
const int MOD=1000000007,INV2=(MOD+1)>>1;
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=605;
int n,a[N],C[N][N],f[N<<1][N],g[N],vis[N<<1];
int main(){
    for(int i=0;i<N;i++){
        C[i][0]=1;
        for(int j=1;j<=i;j++) add(C[i][j]=C[i-1][j-1],C[i-1][j]);
    }
    scanf("%d",&n);
    for(int i=1;i<=n;i++) scanf("%d",&a[i]),vis[a[i]]=1;
    g[0]=1;
    for(int i=1;i<=n;i++) for(int j=0;j<i;j++) add(g[i],(ll)C[i-1][j]*g[j]%MOD*g[i-j-1]%MOD*(i-j+1)%MOD);
    // printf("g\n");
    // for(int i=0;i<=n;i++) printf("%d ",g[i]);printf("\n");
    // printf("f\n");
    f[(n<<1)+1][0]=1;
    for(int i=(n<<1),cnt=0;i>=1;cnt+=!vis[i--]){
        if(!vis[i]) for(int j=0;j<=n;j++) f[i][j]=(ll)(j-cnt)*f[i+1][j]%MOD;
        else for(int j=0;j<=n&&j<=(n<<1)-i-cnt;j++){
            add(f[i][j],f[i+1][j]);
            for(int k=j+1;k<=n;k++) add(f[i][k],(ll)C[(n<<1)-i-cnt-j][k-j-1]*f[i+1][j]%MOD*(k-j+1)%MOD*g[k-j-1]%MOD);
        }
        // for(int j=0;j<=n;j++) printf("%d ",f[i][j]);printf("\n");
    }
    printf("%lld\n",(ll)f[1][n]*qpow(INV2,n)%MOD);
    return 0;
}

QOJ975 Game

与 AGC044E Random Pawn 类似,有方程:

\[E_i=\max(\frac{E_{i-1}+E_{i+1}}2,A_i) \]

求凸包即可。

*P9998 [Ynoi2000] rfrqwq

“看起来不能 polylog。很多涉及集合操作的题目都不能 polylog。” -- 来自本篇

首先要读对题:查询是对单种颜色查询。

首先有一个统计答案的方法。设 \(id_i\) 为 \(i\) 所在连续段编号,\(cntl,cntr\) 分别为查询区间内左右同颜色位置数量,则答案为

\[\sum\limits_{\substack{l\le i<j\le r \\ a_i=a_j=x}}id_j-id_i=\sum\limits_{\substack{l\le i\le r \\ a_i=x}}id_i(cntl_i-cntr_i) \]

考虑序列分块,把 \(id,cntl,cntr\) 拆为块内块外两部分,块内带 \(s\) 后缀,块间带 \(b\) 后缀,且令 \(idb=idb_i,cntb=cntlb_i-cntrb_i\) 这二者与块内 \(i\) 具体值无关。

\[\begin{align*} &\sum\limits_{\substack{i\text{ 在块内} \\ a_i=x}}(ids_i+idb_i)(cntls_i-cntrs_i+cntlb_i-cntrb_i) \\ =&\sum ids_i(cntls_i-cntrs_i)+idb\sum(cntls_i-cntrs_i)+cntb\sum ids_i+idb\cdot cntb \end{align*} \]

维护一下块内每种颜色这几个求和,整块修改只需要推平。

AT_arc068_d [ARC068F] Solitaire

一个合法的 deque,一定是先减后增,中间是 \(1\)。一个合法的最终序列,\(1\) 之前为 deque 两边分别独立取出,即两个递减子序列,\(1\) 之后从一个递增 deque 中取出,为一个递增子序列和一个递减子序列拼接。

观察这四个子序列大小关系:左半段(\(1\) 之前)两个递减子序列,一个接上 \(1\),另一个结尾 \(>\) 右边递减子序列开头;右半段递减子序列结尾 \(>\) 递增子序列结尾。

对于 \(1\) 左右数集确定的情况,右半段的方案数可以直接计算,为 \(2^{n-k-1}\),因为除了最后一次操作,每次可以选择取出最小/最大的数。

进一步,发现左边的两个递减子序列对应的折线不交叉,如果交叉,则可以调整为不交叉的形式。

在不交叉的基础上,考虑能否唯一确定这两个子序列是什么,发现以 \(1\) 结尾的子序列其实就是前缀 \(\min\),另一个就是剩余部分。

考虑 DP(这里或许是本题最难一步,笔者假了若干次),从大到小枚举值域,设 \(f_{i,j}\) 表示枚举到 \(i\),序列上 \(i\) 后面有 \(j\) 个非前缀 \(\min\) 的数且 \(i\) 是前缀 \(\min\)。从 \((k,l)\) 转移到 \((i,j)\),\(i+1\sim k-1\) 必须要放到 \(i\) 后面,故要求 \(i-k-1\le j,l\ge j-(i-k-1)\),即 \(k\ge i-j-1,l-k>j-i\),用一个二维前缀和维护即可做到 \(\mathcal{O}(1)\) 转移。

总复杂度 \(\mathcal{O}(n^2)\)。

上述 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 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=2005;
int n,k,f[N][N],sum[N][N<<1];
int main(){
    scanf("%d%d",&n,&k);
    k=n-k;
    // number has reversed: 1->n,n->1
    f[0][0]=1;
    for(int j=n;j<=(n<<1);j++) sum[0][j]=1;
    for(int i=1;i<=n;i++){
        for(int j=0;j<i;j++){
            add(f[i][j],sum[i-1][n<<1]),add(f[i][j],MOD-sum[i-1][j-i+n]);
            if(i-j-1) add(f[i][j],MOD-sum[i-j-2][n<<1]),add(f[i][j],sum[i-j-2][j-i+n]);
        }
        for(int j=0;j<=n;j++) add(sum[i][j-i+n],f[i][j]);
        for(int j=1;j<=(n<<1);j++) add(sum[i][j],sum[i][j-1]);
        for(int j=0;j<=(n<<1);j++) add(sum[i][j],sum[i-1][j]);
    }
    printf("%lld\n",(ll)(qpow(2,max(k-1,0)))*f[n][k]%MOD);
    return 0;
}

AT_arc078_d [ARC078F] Mole and Abandoned Mine

想到了 \(\mathcal{O}(n3^n)\),想了很久怎么做 \(\mathcal{O}(3^n)\),最终还是差一步没想出来。


观察最终形态,假设已经确定了 \(1\rightsquigarrow n\) 的路径,路径上必然不能有两个点存在其它到达路径,则最终的图应形如一条链 \(1\rightsquigarrow n\),每个点挂了一堆环和连通块(孤立块随便挂到一个结点上即可,不会影响答案,事实上也不会在最终答案中出现)。

对着这个东西随便设计一下状压 DP 就行,有 \(\mathcal{O}(n3^n)\)。


写之前感觉这个东西想过有点悬,尝试做到 \(\mathcal{O}(3^n)\)。考虑分成两部分,一部分是确定路径,这东西可以类似 Hamilton 回路一样做,再设计一个 \(g_S\) 表示已经加入了 \(S\) 中的点之后的最优答案,转移自己只想到了枚举一个 \(T\),再枚举挂在哪个点上,最终仍止步 \(\mathcal{O}(n3^n)\)(代码实现的做法)。学习本篇后,发现可以再维护一个 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=20,W2=(1<<15)+5,W3=14'348'907+5;
int pw[N];
int n,m,G[N][N],vis[N][N],w[W3],id[W2],tmp[N],f[W2][N],g[W2];
vector<pii> vec[W2];
int calcid(int s,int t){
    int x=0;
    for(int i=n-1;i>=0;i--){
        x*=3;
        if((s>>i)&1) x+=1;
        else if((t>>i)&1) x+=2;
    }
    return x;
}
#define lowbit(x) ((x)&(-(x)))
int main(){
    pw[0]=1;
    for(int i=1;i<N;i++) pw[i]=3*pw[i-1];
    scanf("%d%d",&n,&m);
    for(int i=1;i<=m;i++){
        int u,v,w;
        scanf("%d%d%d",&u,&v,&w);
        G[u][v]+=w,G[v][u]+=w;
        vis[u][v]=vis[v][u]=1;
    }
    int mask=(1<<n)-1;
    for(int s=0;s<=mask;s++){
        int ss=mask^s;
        id[ss]=calcid(s,ss);
        for(int i=1;i<=n;i++){
            tmp[i]=0;
            for(int j=1;j<=n;j++) if((s>>(j-1))&1) tmp[i]+=G[i][j];
            if((ss>>(i-1))&1) w[id[ss]]+=tmp[i];
        }
        for(int t=ss;t;t=ss&(t-1)){
            int lb=lowbit(ss^t);
            if(lb){
                int k=31^__builtin_clz(lb);
                id[t]=id[t^lb]-(pw[k]<<1);
                w[id[t]]=w[id[t^lb]]-tmp[k+1];
            }
            vec[s].push_back({t,id[t]});
        }
    }
    // for(int s=0;s<=mask;s++){
    //     for(auto [t,id]:vec[s]){
    //         cout<<"s="<<bitset<4>(s)<<" t="<<bitset<4>(t)<<" w="<<w[id]<<'\n';
    //     }
    // }
    memset(f,0x3f,sizeof(f));
    memset(g,0x3f,sizeof(g));
    f[1][1]=0;
    for(int s=1;s<=mask;s++){
        chkmin(g[s],f[s][n]);
        for(int i=1;i<=n;i++){
            if(!((s>>(i-1))&1)) continue;
            int id=calcid(s^(1<<(i-1)),0);
            for(int j=1;j<=n;j++){
                if((s>>(j-1))&1) continue;
                if(!vis[i][j]) continue;
                chkmin(f[s|(1<<(j-1))][j],f[s][i]+w[id+(pw[j-1]<<1)]);
            }
            for(auto [t,id]:vec[s]) chkmin(g[s|t],g[s]+w[id-pw[i-1]]);
        }
    }
    printf("%d\n",g[mask]);
    return 0;
}

P5354 [Ynoi Easy Round 2017] 由乃的 OJ

简单题。

显然各位独立,考虑从高往低贪心,需要查询某一位在路径上运算完的结果,树剖,合并两个结点信息可以做到 \(\mathcal{O}(1)\)(维护两个二进制数,一个表示所有位输入 \(0\) 的答案,另一个为输入 \(1\) 的答案),总复杂度 \(\mathcal{O}(n+q(k+\log^2n))\)。

*ARC133E Cyclic Medians

好题,被深深震撼了。

首先中位数有经典套路:把 \(<k\) 的数看成 \(0\),\(\ge k\) 的数看成 \(1\),则中位数 \(\ge k\) 当且仅当新的中位数为 \(1\)。

注意到 \(01\) 序列的中位数等于众数。

枚举 \(k\),令 \(x_i\leftarrow [x_i\ge k]\),\(y,a,A\) 同理。

最终的答案为所有 \(k\) 最终结果为 \(1\) 的方案数求和(此处拆贡献,\(\sum k\cdot f_{=k}=\sum f_{\ge k}\))。

对一个时刻 \(t\),考虑 \(a\) 的变化。如果 \(x_{t\mod n}\ne y_{t\mod m}\),那么 \(a\) 不变;否则 \(a\) 变为 \(x_{t\mod n}\)。

不存在 \(x_{t\mod n}=y_{t\mod m}\) 时,最终的结果就是 \(A\),考虑这种情况有多少种。对于 \(i,j\),如果 \(x_i,y_j\) 能同时出现,则 \(\exists t,\text{s.t. }t\equiv i(\mod n),t\equiv j(\mod m)\),类似 exCRT 可知,该条件等价于 \(\text{gcd}(n,m)\mid(i-j)\)。这告诉我们 \(n+m\) 个数形成了若干等价类,对于 \(x_{i+kg},y_{i+kg}\)(其中 \(g=\text{gcd}(n,m)\)),要满足 \(x\) 均为 \(0\),\(y\) 均为 \(1\) 或反之。方案数为

\[S_k=\left((k-1)^{\frac{n}{g}}(V-k+1)^{\frac{m}{g}}+(k-1)^{\frac{m}{g}}(V-k+1)^{\frac{n}{g}}\right)^g \]

存在 \(x_{t\mod n}=y_{t\mod m}\) 时,最终结果是最后一个这样的时刻的数字。设 \(f_{k,0/1}\) 表示阈值为 \(k\) 时,最后一个 \(x_{t\mod n}=y_{t\mod m}\) 为 \(0/1\) 的方案数。注意到有对称性,即 \(f_{k,0}=f_{V-k+2,1}\),两边求和,\(\sum\limits_{k=2}^Vf_{k,0}=\sum\limits_{k=2}^Vf_{k,1}\)。因为 \(f_{k,0}+f_{k,1}+S_k=V^{n+m}\),所以 \(\sum\limits_{k=2}^Vf_{k,1}=\frac12\left((V-1)V^{n+m}-\sum\limits_{k=2}^VS_k\right)\)。

\[ans=\sum\limits_{k=1}^V[A\ge k]S_k+\frac12\left((V-1)V^{n+m}-\sum\limits_{k=2}^VS_k\right)+V^{n+m} \]

P7922 [Kubic] Pyramid

考虑连续段是被提示的,后面是自己想的,代码是 GeMini 3.1-pro 写的(竟然一发过了)。

考虑 a 的连续段,以 A 类操作为例,如果一个连续段两侧都比他大,则长度 -1,如果两侧都比他小,则长度 +1,B 类操作反之。注意到一个连续段如果没有被删空,并且两侧大小关系也不变,则做完一组操作之后其长度不变,用 priority_queue 维护所有“凹”的连续段,每次取出最短的,判断会不会删空,若会,则更新两侧的状态。

考虑求答案,用线段树维护连续段长度和增量,每做一次 A 操作会给长度加上增量,B 操作视为还原,按查询时间排序后线段树上二分,找出查询区间里的连续段,开头和结尾可能是不完整的,维护每个连续段的值,直接计算一下即可。

#include <iostream>
#include <vector>
#include <algorithm>
#include <cstring>

using namespace std;

const int MAX_N = 150005;
const long long INF = 1e18;

int n, m, q;
long long p[MAX_N];
int a[MAX_N];
int op_type[MAX_N]; // 0 for A, 1 for B
int NA[MAX_N], NB[MAX_N], D[MAX_N];

vector<int> D_times[MAX_N];
int first_NA[MAX_N], first_NB[MAX_N];

long long len_base[MAX_N], len_cA[MAX_N], len_cB[MAX_N], len_cD[MAX_N];
long long val_base[MAX_N], val_cA[MAX_N], val_cB[MAX_N], val_cD[MAX_N];

int S;
struct Node {
    int id;
    long long v;
    int tc;
    long long Lc;
    int type; // 0: IP, 1: IV, 2: M, 3: LB_NA, 4: LB_NB, 5: RB_NA, 6: RB_NB
    int prev, next;
} nodes[MAX_N];

int version[MAX_N];

struct Event {
    long long t;
    int id;
    int version;
    bool operator<(const Event& o) const {
        return t > o.t; // min-heap
    }
};

#include <queue>
priority_queue<Event> pq;

void add_BIT(long long* tree, int idx, long long val) {
    if (idx <= 0) return;
    for (; idx <= S; idx += idx & -idx) tree[idx] += val;
}

long long query_BIT(long long* tree, int idx) {
    long long sum = 0;
    for (; idx > 0; idx -= idx & -idx) sum += tree[idx];
    return sum;
}

void update_fenwick(int id, int sign) {
    long long base = 0, cA = 0, cB = 0, cD = 0;
    int type = nodes[id].type;
    long long Lc = nodes[id].Lc;
    int tc = nodes[id].tc;
    if (type == 0) { base = Lc + D[tc]; cD = -1; }
    else if (type == 1) { base = Lc - D[tc]; cD = 1; }
    else if (type == 2) { base = Lc; }
    else if (type == 3 || type == 5) { base = Lc + NA[tc]; cA = -1; }
    else if (type == 4 || type == 6) { base = Lc + NB[tc]; cB = -1; }

    long long v = nodes[id].v;
    add_BIT(len_base, id, sign * base); add_BIT(len_cA, id, sign * cA);
    add_BIT(len_cB, id, sign * cB); add_BIT(len_cD, id, sign * cD);
    
    add_BIT(val_base, id, sign * base * v); add_BIT(val_cA, id, sign * cA * v);
    add_BIT(val_cB, id, sign * cB * v); add_BIT(val_cD, id, sign * cD * v);
}

long long get_death_time(int i) {
    if (nodes[i].Lc == 0) return nodes[i].tc;
    int tc = nodes[i].tc;
    long long Lc = nodes[i].Lc;
    int type = nodes[i].type;
    long long dt = INF;
    if (type == 0) {
        long long V = Lc + D[tc];
        if (V >= 0 && V < MAX_N && !D_times[V].empty()) {
            auto it = upper_bound(D_times[V].begin(), D_times[V].end(), tc);
            if (it != D_times[V].end()) dt = *it;
        }
    } else if (type == 3 || type == 5) {
        long long V = Lc + NA[tc];
        if (V >= 0 && V < MAX_N && first_NA[V] != -1) dt = first_NA[V];
    } else if (type == 4 || type == 6) {
        long long V = Lc + NB[tc];
        if (V >= 0 && V < MAX_N && first_NB[V] != -1) dt = first_NB[V];
    }
    return dt;
}

void set_type(int i) {
    int prev = nodes[i].prev; int next = nodes[i].next;
    long long v = nodes[i].v;
    if (prev == 0 && next == 0) nodes[i].type = 2;
    else if (prev == 0) nodes[i].type = (v > nodes[next].v) ? 3 : 4;
    else if (next == 0) nodes[i].type = (nodes[prev].v < v) ? 5 : 6;
    else {
        long long v_prev = nodes[prev].v, v_next = nodes[next].v;
        if (v_prev < v && v > v_next) nodes[i].type = 0;
        else if (v_prev > v && v < v_next) nodes[i].type = 1;
        else nodes[i].type = 2;
    }
}

long long get_len(int id, int t) {
    int type = nodes[id].type; long long Lc = nodes[id].Lc; int tc = nodes[id].tc;
    if (type == 0) return Lc - (D[t] - D[tc]);
    if (type == 1) return Lc + (D[t] - D[tc]);
    if (type == 2) return Lc;
    if (type == 3 || type == 5) return Lc - (NA[t] - NA[tc]);
    if (type == 4 || type == 6) return Lc - (NB[t] - NB[tc]);
    return 0;
}

void process_death(int u, int t) {
    int prev = nodes[u].prev; int next = nodes[u].next;
    update_fenwick(u, -1); version[u]++;
    
    if (prev != 0 && next != 0) {
        update_fenwick(prev, -1); update_fenwick(next, -1);
        long long L_prev = get_len(prev, t); long long L_next = get_len(next, t);
        
        if (nodes[prev].v == nodes[next].v) {
            nodes[prev].tc = t; nodes[prev].Lc = L_prev + L_next; nodes[prev].next = nodes[next].next;
            if (nodes[next].next != 0) nodes[nodes[next].next].prev = prev;
            version[prev]++; version[next]++;
            set_type(prev); update_fenwick(prev, 1);
            long long dt = get_death_time(prev); if (dt <= n - 1) pq.push({dt, prev, version[prev]});
        } else {
            nodes[prev].tc = t; nodes[prev].Lc = L_prev; nodes[next].tc = t; nodes[next].Lc = L_next;
            nodes[prev].next = next; nodes[next].prev = prev;
            version[prev]++; version[next]++;
            set_type(prev); set_type(next); update_fenwick(prev, 1); update_fenwick(next, 1);
            long long dt_prev = get_death_time(prev); if (dt_prev <= n - 1) pq.push({dt_prev, prev, version[prev]});
            long long dt_next = get_death_time(next); if (dt_next <= n - 1) pq.push({dt_next, next, version[next]});
        }
    } else if (prev != 0) {
        update_fenwick(prev, -1); long long L_prev = get_len(prev, t);
        nodes[prev].tc = t; nodes[prev].Lc = L_prev; nodes[prev].next = 0; version[prev]++;
        set_type(prev); update_fenwick(prev, 1);
        long long dt = get_death_time(prev); if (dt <= n - 1) pq.push({dt, prev, version[prev]});
    } else if (next != 0) {
        update_fenwick(next, -1); long long L_next = get_len(next, t);
        nodes[next].tc = t; nodes[next].Lc = L_next; nodes[next].prev = 0; version[next]++;
        set_type(next); update_fenwick(next, 1);
        long long dt = get_death_time(next); if (dt <= n - 1) pq.push({dt, next, version[next]});
    }
}

int find_segment(long long p_target, int x) {
    long long current_len = 0; int pos = 0;
    for (int i = 18; i >= 0; --i) {
        int next_pos = pos + (1 << i);
        if (next_pos <= S) {
            long long len_add = len_base[next_pos] + len_cA[next_pos] * NA[x] + len_cB[next_pos] * NB[x] + len_cD[next_pos] * D[x];
            if (current_len + len_add < p_target) {
                current_len += len_add;
                pos = next_pos;
            }
        }
    }
    return pos + 1;
}

long long query_len(int pos, int x) {
    return query_BIT(len_base, pos) + query_BIT(len_cA, pos) * NA[x] + query_BIT(len_cB, pos) * NB[x] + query_BIT(len_cD, pos) * D[x];
}

long long query_val(int pos, int x) {
    return query_BIT(val_base, pos) + query_BIT(val_cA, pos) * NA[x] + query_BIT(val_cB, pos) * NB[x] + query_BIT(val_cD, pos) * D[x];
}

struct Query { int x, l, r, id; bool operator<(const Query& o) const { return x < o.x; } };

int main() {
    ios_base::sync_with_stdio(false);
    cin.tie(NULL);

    if (!(cin >> n >> m >> q)) return 0;
    for (int i = 1; i <= n; ++i) cin >> p[i];
    for (int i = 1; i <= m; ++i) cin >> a[i];

    int M = n - 1, t = 0;
    for (int i = 1; i <= m; ++i) {
        for (int j = 0; j < a[i]; ++j) op_type[++t] = 0;
        for (int j = 0; j < a[i]; ++j) op_type[++t] = 1;
    }

    memset(first_NA, -1, sizeof(first_NA));
    memset(first_NB, -1, sizeof(first_NB));
    first_NA[0] = 0; first_NB[0] = 0; D_times[0].push_back(0);

    for (int i = 1; i <= M; ++i) {
        NA[i] = NA[i - 1] + (op_type[i] == 0);
        NB[i] = NB[i - 1] + (op_type[i] == 1);
        D[i] = NA[i] - NB[i];
        if (first_NA[NA[i]] == -1) first_NA[NA[i]] = i;
        if (first_NB[NB[i]] == -1) first_NB[NB[i]] = i;
        D_times[D[i]].push_back(i);
    }

    vector<pair<int, int>> segs;
    for (int i = 1; i <= n; ) {
        int j = i; while (j <= n && p[j] == p[i]) j++;
        segs.push_back({p[i], j - i}); i = j;
    }
    S = segs.size();

    for (int i = 1; i <= S; ++i) {
        nodes[i].id = i; nodes[i].v = segs[i - 1].first; nodes[i].tc = 0; nodes[i].Lc = segs[i - 1].second;
        nodes[i].prev = (i == 1) ? 0 : i - 1; nodes[i].next = (i == S) ? 0 : i + 1;
        version[i] = 0;
    }

    for (int i = 1; i <= S; ++i) {
        set_type(i); update_fenwick(i, 1);
        long long dt = get_death_time(i); if (dt <= M) pq.push({dt, i, version[i]});
    }

    vector<Query> qs(q);
    for (int i = 0; i < q; ++i) { cin >> qs[i].x >> qs[i].l >> qs[i].r; qs[i].id = i; }
    sort(qs.begin(), qs.end());

    vector<long long> ans(q);
    for (const auto& Q : qs) {
        while (!pq.empty() && pq.top().t <= Q.x) {
            Event ev = pq.top(); pq.pop();
            if (ev.version != version[ev.id]) continue;
            process_death(ev.id, ev.t);
        }

        int Kl = find_segment(Q.l, Q.x), Kr = find_segment(Q.r, Q.x);
        long long res = 0;
        if (Kl == Kr) {
            res = (long long)(Q.r - Q.l + 1) * nodes[Kl].v;
        } else {
            long long rem_L = query_len(Kl, Q.x) - Q.l + 1; res += rem_L * nodes[Kl].v;
            long long rem_R = Q.r - query_len(Kr - 1, Q.x); res += rem_R * nodes[Kr].v;
            if (Kl + 1 <= Kr - 1) res += query_val(Kr - 1, Q.x) - query_val(Kl, Q.x);
        }
        ans[Q.id] = res;
    }

    for (int i = 0; i < q; ++i) cout << ans[i] << "\n";
    return 0;
}
posted @ 2026-02-06 16:35  SmpaelFx  阅读(37)  评论(0)    收藏  举报