好题收集

好题,比较有 trick/套路的题,印象深刻的题

P4240 毒瘤之神的考验

拆式子与根号分治思想的极致融合。

\(n,m\),求

\[\sum\limits_{i=1}^n\sum\limits_{j=1}^m \varphi(ij) \]

对 998244353 取模,多测。

\(n,m\le 10^5,T\le 10^4\)

\(n\le m\)

先要知道一个经典的式子:

\[\varphi(ij)=\frac{\varphi(i)\varphi(j)\gcd(i,j)}{\varphi(\gcd(i,j))} \]

然后拆式子

\[\begin{aligned} \sum\limits_{i=1}^n\sum\limits_{j=1}^m \varphi(ij)&=\sum\limits_{i=1}^n\sum\limits_{j=1}^m \frac{\varphi(i)\varphi(j)\gcd(i,j)}{\varphi(\gcd(i,j))} \end{aligned}\]

枚举 \(d=\gcd(i,j)\)

\[\begin{aligned} \sum\limits_{i=1}^n\sum\limits_{j=1}^m \frac{\varphi(i)\varphi(j)\gcd(i,j)}{\varphi(\gcd(i,j))}&=\sum\limits_{d=1}^n\sum\limits_{i=1}^n\sum\limits_{j=1}^m \frac{\varphi(i)\varphi(j)d[\gcd(i,j)=d]}{\varphi(d)}\\ &=\sum\limits_{d=1}^n\frac{d}{\varphi(d)}\sum\limits_{i=1}^n\sum\limits_{j=1}^m \varphi(i)\varphi(j)[\gcd(i,j)=d]\\ &=\sum\limits_{d=1}^{n}\frac{d}{\varphi(d)}\sum\limits_{i=1}^{n/d}\sum\limits_{j=1}^{m/d} \varphi(id)\varphi(jd)[\gcd(i,j)=1]\\ \end{aligned}\]

后面那个直接莫反拆掉,把枚举扔外面

\[\begin{aligned} \sum\limits_{d=1}^{n}\frac{d}{\varphi(d)}\sum\limits_{i=1}^{n/d}\sum\limits_{j=1}^{m/d} \varphi(id)\varphi(jd)[\gcd(i,j)=1]&=\sum\limits_{d=1}^{n}\frac{d}{\varphi(d)}\sum\limits_{i=1}^{n/d}\sum\limits_{j=1}^{m/d} \varphi(id)\varphi(jd)\sum\limits_{t\mid i,t\mid j}\mu(t)\\ &=\sum\limits_{d=1}^{n}\frac{d}{\varphi(d)}\sum\limits_{t=1}^{n/d}\mu(t)\sum\limits_{i=1}^{n/d}\sum\limits_{j=1}^{m/d} \varphi(idt)\varphi(jdt)\\ &=\sum\limits_{t=1}^{n}\sum\limits_{d\mid t}\frac{d\mu(\frac{t}{d})}{\varphi(d)}\sum\limits_{i=1}^{n/t}\varphi(it)\sum\limits_{j=1}^{m/t} \varphi(jt)\\ \end{aligned}\]

这个式子就很好看了,设

\[f(n)=\sum\limits_{d|n}\frac{d\mu(\frac{n}{d})}{\varphi(d)},g(n,k)=\sum\limits_{i=1}^{n}\varphi(ik) \]

则原式为

\[\sum\limits_{t=1}^n f(t)g(n/t,t)g(m/t,t) \]

\(f,g\) 直接预处理是可以的,因为 \(g\) 的数量是 \(n\log n\) 级别的,且 \(g\) 可以直接递推:

\[g(n,k)=g(n-1,k)+\varphi(nk) \]

所以预处理 \(f,g\) 时间复杂度 \(O(n\log n)\)

由于原式并不好整除分块,也不能暴力预处理全部信息,所以我们只能考虑分治。

设阈值为 \(B\),记原式为三元函数 \(h(a,b,n)=\sum\limits_{t=1}^n f(t)g(a,t)g(b,t)\),考虑当 \(a,b\le B\) 时直接暴力预处理答案,时空复杂度 \(O(nB^2)\),这个三元函数就可以数论分块了,即

\[\sum\limits_{n/l=m/l,n/r=m/r}h(n/l,m/l,r)-h(n/l,m/l,l-1) \]

\(h\) 也可以递推

\[h(a,b,n)=h(a,b,n-1)+f(n)g(a,n)g(b,n) \]

否则可以得到 \(n/a\ge B,a\le n/B\),暴力统计答案,时间复杂度 \(O(n/B)\)

综上,时间复杂度 \(O(n\log n+nB^2+T(\sqrt n+\frac{n}{B}))\),取 \(B=\sqrt[3]{T}=22\),可以通过,500~700 ms,空间复杂度 \(O(n\log n+nB^2)\)

title: code
collapse: close
```cpp
#include<bits/stdc++.h>
using namespace std;
using ll=long long;
const int maxn=1e5+7;
const int B=24;
const int maxb=B+2;
const int N=1e5;
const int mod=998244353;
bool st;
int mu[maxn],phi[maxn],pr[maxn],pcnt;
ll f[maxn],inv[maxn];
bool isp[maxn];
vector<ll>g[maxn],h[maxb][maxb];
bool ed;
void solve(){
    int n,m,l=1,r;
    cin>>n>>m;
    if(n>m) swap(n,m);
    ll ans=0;
    for(;l<=m/B;l++)
        ans=(ans+f[l]*g[l][n/l]%mod*g[l][m/l]%mod)%mod;
    for(;l<=n;l=r+1){
        r=min(n/(n/l),m/(m/l));
        ans=(ans+h[n/l][m/l][r]-h[n/l][m/l][l-1]+mod)%mod;
    }
    cout<<ans<<'\n';
}
signed main(){
    cerr<<(&ed-&st)/1048576.0<<" MB\n";
    inv[1]=mu[1]=phi[1]=1;
    for(int i=2;i<=N;i++){
        inv[i]=(mod-mod/i)*inv[mod%i]%mod;
        if(!isp[i]) pr[++pcnt]=i,mu[i]=-1,phi[i]=i-1;
        for(int j=1;j<=pcnt&&i*pr[j]<=N;j++){
            isp[i*pr[j]]=1;
            if(i%pr[j]==0){
                mu[i*pr[j]]=0;
                phi[i*pr[j]]=pr[j]*phi[i];
                break;
            }
            mu[i*pr[j]]=-mu[i];
            phi[i*pr[j]]=phi[i]*phi[pr[j]];
        }
    }
    for(int i=1;i<=N;i++){
        g[i].resize(N/i+2); g[i][0]=0;
        for(int j=1;j<=N/i;j++)
            g[i][j]=(g[i][j-1]+phi[i*j])%mod;
    }
    for(int i=1;i<=N;i++)
        for(int j=1;j<=N/i;j++)
            f[j*i]=(f[j*i]+i*mu[j]*inv[phi[i]]%mod+mod)%mod;
    for(int j=1;j<=B;j++){
        for(int k=1;k<=B;k++){
            h[j][k].resize(N/k+7); h[j][k][0]=0;
            for(int i=1;i<=N/k;i++)
                h[j][k][i]=(h[j][k][i-1]+f[i]*g[i][j]%mod*g[i][k]%mod)%mod;
        }
    }
    int TEST;
    cin>>TEST;
    while(TEST--){
        solve();
    }
    return 0;
}

CF1491H Yuezheng Ling and Dynamic Tree

*3400

还是分块大佬

信友队还是放了一道可做题。

给你一棵树,\(i\)\(fa(i)(<i)\) 连边,\(q\) 次操作:

  • \(\forall i\in[l,r](l>1)\)\(fa(i)\) 变为 \(\max(fa(i)-x,1)\)
  • \(u,v\) 的 LCA。

\(n,q\le 10^5,x\ge 1\)

考虑按下标分块,记 \(jmp(i)\) 表示点 \(i\) 第一个通过走父亲跳出块的下标,显然有 \(jmp(i)<i\)

对于修改,散块暴力重构,整块也暴力重构——注意到 \(x\ge 1\),所以每个块至多进行 \(O(B)\) 次暴力修改后就满足对于所有块内的点一次就可以跳出去(即 \(block(fa(i))<block(i)\)),这有什么用呢?这说明,之后的更改对于块内的所有数都是同步的,与实际数无关,于是我们可以记个 tag 表示当前整块的懒标记,前 \(O(B)\) 次暴力下传,后面的就不用了。时间复杂度均摊 \(O(B)\)

对于查询,类似树剖,交替跳 \(jmp\),每次跳下标大的。如果跳到一个块里了,且两个数的 \(jmp\) 相等,就再暴力跳 LCA 即可。时间复杂度 \(O(\frac{n}{B}+B)\)

综上时间复杂度 \(O(n+q(\frac{n}{B}+B))\),取 \(B=\sqrt n\)

title: code
collapse: close
```cpp
#include<bits/stdc++.h>
using namespace std;
const int maxn=1e5+7;
int B,n,q,bcnt;
int fa[maxn],st[maxn],ed[maxn],bl[maxn],jmp[maxn],ktag[maxn],siz[maxn],add[maxn];
void dfs(int u){
    int now=bl[u],tmp=u;
    for(;u&&bl[u]==now&&!jmp[u];u=fa[u]);
    if(bl[u]!=now){
        int to=u; u=tmp;
        for(;u&&bl[u]==now;u=fa[u]) jmp[u]=to;
    }else{
        int to=u; u=tmp;
        for(;u&&!jmp[u];u=fa[u]) jmp[u]=jmp[to];
    }
}
void pushdown(int id){
    for(int i=st[id];i<=ed[id];i++){
        if(i==1) continue;
        fa[i]=max(fa[i]-add[id],1);
    }
    add[id]=0;
    for(int i=st[id];i<=ed[id];i++){
        if(bl[fa[i]]!=bl[i]) jmp[i]=fa[i];
        else jmp[i]=jmp[fa[i]];
    }
}
signed main(){
    ios::sync_with_stdio(0);
    cin.tie(0); cout.tie(0);
    cin>>n>>q;
    B=sqrt(n)+1;
    bcnt=(n-1)/B+1;
    bl[1]=1;
    for(int i=2;i<=n;i++){
        cin>>fa[i];
        bl[i]=(i-1)/B+1;
    }
    for(int i=1,j=1;i<=n;i+=B,j++){
        st[j]=i;
        ed[j-1]=i-1;
        siz[j]=B;
    }
    ed[bcnt]=n;siz[bcnt]=ed[bcnt]-st[bcnt]+1;
    for(int i=1;i<=bcnt;i++)
        for(int j=st[i];j<=ed[i];j++)
            if(!jmp[j]) dfs(j);
    while(q--){
        int op,x,y,z;   
        cin>>op>>x>>y;
        if(op==1){
            cin>>z;
            if(bl[x]==bl[y]){
                for(int i=x;i<=y;i++) 
                    fa[i]=max(fa[i]-z,1);
                pushdown(bl[x]);
            }else{
                for(int i=x;i<=ed[bl[x]];i++)
                    fa[i]=max(fa[i]-z,1);
                pushdown(bl[x]);
                add[bl[x]]=0;
                for(int j=bl[x]+1;j<=bl[y]-1;j++){
                    if(add[j]<=n) add[j]+=z;
                    ktag[j]++;
                    if(ktag[j]<=siz[j]) pushdown(j);
                }
                for(int i=st[bl[y]];i<=y;i++)
                    fa[i]=max(fa[i]-z,1);
                pushdown(bl[y]);
            }
        }else{
            while(1){
                if(x<y) swap(x,y);
                if(bl[x]!=bl[y]) x=max(jmp[x]-add[bl[x]],1);
                else{
                    if(max(jmp[x]-add[bl[x]],1)!=max(jmp[y]-add[bl[y]],1)) 
                        x=max(jmp[x]-add[bl[x]],1),y=max(jmp[y]-add[bl[y]],1);
                    else break;
                }
            }
            while(x!=y){
                if(x<y) swap(x,y);
                x=max(fa[x]-add[bl[x]],1);
            }
            cout<<x<<'\n';
        }
    }
    return 0;
}

ARC186B Typical Permutation Descriptor

这棵树这么好看感觉很典啊,wc 怎么是树上拓扑序计数板子

给你一个序列 \(a\) 满足 \(a_i<i\),求满足以下条件的排列 \(p\) 的数量:

  • \(p_j>p_i>p_{a_i}(j\in(a_i,i))\)

\(n\le 3\times 10^5\)保证有解

由于保证有解,考虑观察有解的情况所带来的性质:

  1. 区间 \([a_i,i]\) 要么把前面的若干区间完全包含,要么左端点与相邻区间端点相交;

由性质 1 与偏序关系可知,假如以偏序关系(大于号连接的两边)连边,\(p_i\) 为点权,以 \(p_0\) 为根,则形成一棵满足 \(u\) 子树内的点权大于 \(u\) 点权的树。(由不交想到转化为树上问题)

这棵树的性质很好啊,当你用拓扑序遍历这棵树他一定合法,即为充分必要条件了,虽然我没看出来

接下来就是一个裸的树上拓扑序计数了,也是个结论,即

\[\frac{n!}{\prod\limits_{i=1}^n siz(i)} \]

证明:
考虑树形 DP。设 \(f(u)\) 为以 \(u\) 为根子树的拓扑序数量。
考虑合并两棵子树 \(v_1,v_2\),先把两棵子树的方案数乘起来然后考虑顺序,即在 \(siz(v_1)+siz(v_2)\) 个数里选掉 \(siz(v_1)\) 个数。更一般的,多个子树相当于叠加,而且组合约掉了,则有转移

\[\begin{aligned} f(u)&=\binom{siz(v_1)+siz(v_2)}{siz(v_1)}\binom{siz(v_1)+siz(v_2)+siz(v_3)}{siz(v_1)+siz(v_2)}\cdots\binom{siz(u)-1}{siz(v_1)+siz(v_2)+\cdots+siz(v_{x-1})}\prod\limits_{v\in son(u)}f(v)\\ &=\frac{(siz(u)-1)!}{\prod\limits_{v\in son(u)}siz(v)!}\prod\limits_{v\in son(u)}f(v)\\ &=\frac{(siz(u)-1)!}{\prod\limits_{v\in son(u)}siz(v)!}\prod\limits_{v\in son(u)}f(v)\\ \end{aligned}\]

考虑把每个 \((siz(u)-1)!\)\(siz(v)!\) 相抵消,剩下 \(\prod\limits_{i=2}^n\frac{1}{siz(i)}\) 以及 \((siz(1)-1)!\),写得好看点,都乘个 \(siz(1)\),即得上式。

时间复杂度 \(O(n)\)

title: code
collapse: close
```cpp
#include<bits/stdc++.h>
using namespace std;
#define int long long
const int maxn=3e5+7;
const int mod=998244353;
int n,a[maxn],siz[maxn],inv[maxn];
vector<int>v[maxn],e[maxn];
int facn=1;
void dfs(int u,int fa){siz[u]=u>0;for(int v:e[u])if(v!=fa){dfs(v,u);siz[u]+=siz[v];}}
signed main(){
    cin>>n;
    for(int i=1;i<=n;i++){
        cin>>a[i];
        v[a[i]].emplace_back(i);
        facn=facn*i%mod;
    }
    queue<int>q;
    q.push(0);
    while(!q.empty()){
        int now=q.front();
        q.pop();
        if(!v[now].empty()){
            q.push(v[now].back());
            e[now].emplace_back(v[now].back());
            v[now].pop_back(); 
        }
        if(now&&!v[a[now]].empty()){
            q.push(v[a[now]].back());
            e[now].emplace_back(v[a[now]].back());
            v[a[now]].pop_back(); 
        }
    }
    dfs(0,0);
    inv[1]=1;
    for(int i=2;i<=n;i++)
        inv[i]=(mod-mod/i)*inv[mod%i]%mod;
    for(int i=1;i<=n;i++)
        facn=facn*inv[siz[i]]%mod;
    cout<<facn;
    return 0;
}

P3332 [ZJOI2013] K大数查询

线段树套权值线段树怎么下传标记啊,怎么tj都没有这种写法啊,哦反着套就行了。笑点解析:权值线段树的作用 = 整体二分。

维护 \(n\) 个 multiset,支持以下操作:

  • 在编号为 \([l,r]\) 的 multiset 中加入 \(c\)
  • 查询 \(\bigcup\limits_{i=l}^r s_i\) 的第 \(k\)

\(|c|\le n\le 5\times 10^4,k< 2^{63}\)

非常显然的一个想法就是线段树套权值线段树:线段树每个节点开一棵权值线段树,每次在节点上打懒标记并单点修改。但是标记无法叠加。时空直接爆炸。

考虑权值线段树套线段树。每个节点维护值在 \([L,R]\) 范围内的全局线段树,每次修改相当于将包含 \([c,c]\) 的节点上的 \([l,r]\) 区间加 1.查询直接看当前节点在区间 \([l,r]\) 中的数的数量,跑 kth 即可。时空复杂度 \(O(n\log^2 n)\),空间至少开 512 倍,或者 vector。

注意 \(c\) 可能为负。

title: code
collapse: close
```cpp
#include<bits/stdc++.h>
using namespace std;
#define lc tr[now].ls
#define rc tr[now].rs
#define mid ((l+r)>>1)
#define lson (now<<1)
#define rson (now<<1|1)
const int maxn=1e5+14;
const int N=5e4+3;
using ll=long long;
struct node{
    int ls,rs;
    ll val,tag;
}tr[maxn<<8];
int n,m,opt[maxn],L[maxn],R[maxn],root[maxn<<2];
ll q[maxn];
int tot;
int addnode(){
    tr[++tot]={0,0,0,0};
    return tot;
}
void pushup(int now){
    tr[now].val=tr[lc].val+tr[rc].val;
}
void pushdown(int now,int l,int r){
    if(!tr[now].tag) return;
    if(!lc) lc=addnode();
    if(!rc) rc=addnode();
    tr[lc].tag+=tr[now].tag;
    tr[rc].tag+=tr[now].tag;
    tr[lc].val+=tr[now].tag*(mid-l+1);
    tr[rc].val+=tr[now].tag*(r-mid);
    tr[now].tag=0;
}
void modi(int &now,int l,int r,int L,int R,ll x){
    if(!now) now=addnode();
    if(L<=l&&r<=R){
        tr[now].tag+=x;
        tr[now].val+=x*(r-l+1);
        return;
    }
    pushdown(now,l,r);
    if(L<=mid) modi(lc,l,mid,L,R,x);
    if(mid+1<=R) modi(rc,mid+1,r,L,R,x);
    pushup(now);
}
ll qu(int &now,int l,int r,int L,int R){
    if(!now) now=addnode();
    if(L<=l&&r<=R) return tr[now].val;
    pushdown(now,l,r);
    ll res=0;
    if(L<=mid) res+=qu(lc,l,mid,L,R);
    if(mid+1<=R) res+=qu(rc,mid+1,r,L,R);
    return res;
}
void modify(int now,int l,int r,int L,int R,ll x){
    modi(root[now],1,n,L,R,1);
    if(l==r) return;
    if(x<=mid) modify(lson,l,mid,L,R,x);
    else modify(rson,mid+1,r,L,R,x);
}
int query(int now,int l,int r,int L,int R,ll x){
    if(l==r)  return l;
    ll res=qu(root[rson],1,n,L,R);
    if(x<=res) return query(rson,mid+1,r,L,R,x);
    else return query(lson,l,mid,L,R,x-res);
}
signed main(){
    cin>>n>>m;
    for(int i=1;i<=m;i++){
		cin>>opt[i]>>L[i]>>R[i]>>q[i];
    }
    for(int i=1;i<=m;i++){
		if(opt[i]==1){
            modify(1,1,2*N,L[i],R[i],q[i]+N);
        }else if(opt[i]==2){
            cout<<query(1,1,2*N,L[i],R[i],q[i])-N<<'\n';
		}
	}
    return 0;
}

P3527 [POI2011] MET-Meteors

整体二分的题都可以树套树做。——includer

唉我不会。

有一个长为 \(m\) 的环,每个位置都有一个颜色 \(c_i\),执行 \(q\) 次操作将区间 \([l_i,r_i]\) 的值 \(v_i\) 加上 \(a_i\),每个颜色有一个要求 \(h_i\)。对于每个颜色,求出其最早满足 \(v_i\ge h_i\) 的操作编号 \(p_i\)

\(n,m,q\le 3\times 10^5\)

考虑我们暴力怎么做。对于每个颜色,二分 \(p_i\),然后做覆盖,时间复杂度 \(O(n^2)\)

整体二分,相当于同时对所有询问二分。类似分治/归并思想。对于当前区间的一组询问 \(\{l,r,Q\}\),check mid 时 \(Q_i\) 是否合法,若合法则 \(Q_i\) 的答案一定 \(\le mid\),否则 \(>mid\),据此分成 \(\{l,mid,Q_l\},\{mid+1,r,Q_r\}\) 两部分继续递归。总共递归 \(O(\log n)\) 层,时间复杂度 \(O(n\log n F(n)+q\log nF(n))\)\(F(n)\) 指单次 check 的复杂度。

注意: 递归右区间时需要保留左边的影响。

void solve(int l,int r,vector<int>q){ 
    // 用 vector 会慢一点,可以考虑开一个全局数组,并传当前的区间询问所在的下标区间 [L,R]
    if(l==r){
        for(int i:q) ans[i]=l;
        return;
    }
    int mid=(l+r)>>1;
    vector<int>v1,v2;
    insert(l,mid);
    for(int i:q){
        if(check(i)) v1.push_back(i);
        else v2.push_back(i);
    }
    solve(mid+1,r,v2); // 区间 [mid+1,r] 不需要撤销影响 
    erase(l,mid); 
    solve(l,mid,v1); // 区间 [l,mid] 需要撤销影响
}

整体二分都很板,只要写出 check 就差不多了。本题就相当于每次执行 \([l,mid]\) 的操作。判断所有位置颜色的和是否大于等于 \(h_i\),然后撤销即可。这个用一个差分树状数组维护即可。时间复杂度 \(O(n\log^2 n)\)

title: code
collapse: close
```cpp
#include<bits/stdc++.h>
using namespace std;
const int maxn=3e5+7;
const int N=3e5+3;
int tr[maxn];
int lowbit(int x){return x&-x;}
void add(int x,int c){for(int i=x;i<=N;i+=lowbit(i)) tr[i]+=c;}
int query(int x){
    int res=0;
    for(int i=x;i;i-=lowbit(i)) res+=tr[i];
    return res;
}
int n,m,a[maxn],l[maxn],r[maxn],h[maxn],Q,ans[maxn];
vector<int>v[maxn];
void rain(int id){
    if(l[id]<=r[id]) add(l[id],a[id]), add(r[id]+1,-a[id]);
    else add(1,a[id]),add(r[id]+1,-a[id]),add(l[id],a[id]),add(m+1,-a[id]);
}
void quash(int id){
    if(l[id]<=r[id])  add(l[id],-a[id]), add(r[id]+1,a[id]);
    else add(1,-a[id]),add(r[id]+1,a[id]),add(l[id],-a[id]),add(m+1,a[id]);
}
void solve(int l,int r,vector<int>q){
    if(l==r){
        for(int i:q) ans[i]=l;
        return;
    }
    int mid=(l+r)>>1;
    vector<int>v1,v2;
    for(int i=l;i<=mid;i++) rain(i);
    for(int i:q){
        int sum=0;
        for(int j:v[i]) sum+=query(j);
        if(sum>=h[i]) v1.push_back(i);
        else v2.push_back(i);
    }
    solve(mid+1,r,v2);
    for(int i=l;i<=mid;i++) quash(i);
    solve(l,mid,v1); 
}
signed main(){
    cin>>n>>m;
    for(int i=1;i<=m;i++){
        cin>>a[i];
        v[a[i]].push_back(i);
    }
    for(int i=1;i<=n;i++){
        cin>>h[i];
    }
    cin>>Q;
    for(int i=1;i<=Q;i++){
        cin>>l[i]>>r[i]>>a[i];
    }
    vector<int>t;
    for(int i=1;i<=n;i++) t.push_back(i);
    solve(1,Q+1,t);
    for(int i=1;i<=n;i++){
        if(ans[i]!=Q+1)
        cout<<ans[i]<<'\n';
        else cout<<"NIE\n";
    }
    return 0;
}

P9870 [NOIP2023] 双序列拓展

NOIP 出这个我原地退役,这么喜欢 Ad-hoc

给你两个序列 \(a_n,b_m\),询问是否存在一组 \(a,b\)扩展 \(A_L,B_L(L=+\infty)\) 满足 \(\forall(A_i-B_i)(A_j-B_j)>0,i,j\in[1,+\infty)\)

\(q\) 次修改 \(a,b\) 中的一些数。

\(a_n\)扩展 \(A_L\) 定义为存在一个序列 \(t=\{t_1,t_2,\cdots,t_n\}\) 满足 \(\sum t_i=L\)\(A_L=\{a_1\times t_1+a_2\times t_2+\cdots+a_n\times t_n\}\),其中 \(+,\times\) 表示拼接与重复。

\(n,m\le 5\times 10^5,q\le 50\)

逆天 Ad-hoc。建议 FTR 11/AT 15.9。

与扩展长度相关的算法显然先要枪毙。然后就不会了。

不是哥们。

考虑序列中的数的种类至多 \(n+m\) 个,考虑设计相关复杂度的算法。

\(\forall(A_i-B_i)(A_j-B_j)>0\) 唉这个我认识。即要满足 \(\forall i,A_i<B_i\)\(\forall i,A_i>B_i\),这两种本质一样。先考虑第一种。

考虑到我们只有边界条件要关心,其余的位置便自动合法。当扩展长度 \(L\to L+1\) 时,设当前 \(a\) 使用的是 \(a_i\) 扩展,\(b\) 使用的是 \(b_j\) 扩展,则只会出现四种情况:

  • \((i,j)\to(i,j)\):还是使用原先的两个,所以不是边界,不用管;
  • \((i,j)\to(i+1,j)\)\(a\) 变为使用 \(a_{i+1}\),这种情况需要满足 \(a_{i+1}<b_j\) 才能转移;
  • \((i,j)\to(i,j+1)\)\(b\) 变为使用 \(b_{j+1}\),这种情况需要满足 \(a_{i}<b_{j+1}\) 才能转移;
  • \((i,j)\to(i+1,j+1)\)\(a\) 变为使用 \(a_{i+1}\)\(b\) 变为使用 \(b_{j+1}\),这种情况需要满足 \(a_{i+1}<b_{j+1}\) 才能转移;

转移都出来了,直接 DP 是 \(O(qnm)\) 的,可以得 35 分。设 \(f(i,j)\) 表示当前 \(a\) 使用的是 \(a_i\) 扩展,\(b\) 使用的是 \(b_j\) 扩展是否合法,则有:

\[f(i,j)=[a_i<b_j]\And(f(i-1,j)\mid f(i,j-1)\mid f(i-1,j-1)) \]

很不能优化的样子。考虑上面在干啥。

相当于有一个矩阵 \(c_{i,j}=[a_i<b_j]\) 要从 \((1,1)\) 走到 \((n,m)\) 可以向右、下、右下走,且要满足 \(c_{i,j}=1\)

考虑什么时候无解。当 \(a_{\min}\ge b_{\min}/b_{\max}\le a_{\max}\)\(b_{\min}/a_{\max}\) 那一列/排的 \(c\) 都为 0,所以无解。

特殊性质:\(a_n \ll a_1<b_1\ll b_m\),所以有 \(a_n< \forall b_j,b_m>\forall a_i\)。即最后一行/列都是 1。所以我们只要到达最后一行/列即可。

我们只要走到 \(n-1\) 行或 \(m-1\) 列就到了。递归地看,问题变小。然后其实变成了上面的子问题。直接递归求解即可(合法情况下,即 \(\exists a_i=a_{\min}<\forall b_j=b_{\min}/\exists b_j=b_{\max}>\forall a_i=a_{\max}\),说明我们只要走到 \(i\) 行/\(j\) 列即可,再次缩小范围求解)。

没有特殊性质也一样。仅第一步不同而已。预处理前/后缀 \(\min/\max\),时间复杂度 \(O(q(n+m))\)

title: code
collapse: close
```cpp
#include<bits/stdc++.h>
using namespace std;
const int maxn=5e5+7;
int a[maxn],b[maxn],f[maxn],g[maxn],ta[maxn],tb[maxn];
#define get(A,p) {A[i]>A[p.mx] ? i : p.mx, A[i]<A[p.mi] ? i : p.mi} 
struct node{
    int mx,mi;
    node(int x=0,int y=0): mx(x),mi(y){}
}pren[maxn],prem[maxn],sufn[maxn],sufm[maxn];
void update(int n,int m){
    pren[1]={1,1}; sufn[n]={n,n};
    prem[1]={1,1}; sufm[m]={m,m};
    for(int i=2;i<=n;i++) pren[i]=get(f,pren[i-1]);
    for(int i=2;i<=m;i++) prem[i]=get(g,prem[i-1]);
    for(int i=n-1;i ;i--) sufn[i]=get(f,sufn[i+1]);
    for(int i=m-1;i ;i--) sufm[i]=get(g,sufm[i+1]);
}
bool check1(int x,int y,int n,int m){
    if(x==1||y==1) return 1;
    node X=pren[x-1],Y=prem[y-1];
    if(f[X.mi]<g[Y.mi]) return check1(X.mi,y,n,m);
    if(f[X.mx]<g[Y.mx]) return check1(x,Y.mx,n,m);
    return 0;
}
bool check2(int x,int y,int n,int m){
    if(x==n||y==m) return 1;
    node X=sufn[x+1],Y=sufm[y+1];
    if(f[X.mi]<g[Y.mi]) return check2(X.mi,y,n,m);
    if(f[X.mx]<g[Y.mx]) return check2(x,Y.mx,n,m);
    return 0;
}
bool solve(int tta[],int ttb[],int n,int m){
    if(tta[1]>=ttb[1]) return 0;
    for(int i=1;i<=n;i++) f[i]=tta[i];
    for(int i=1;i<=m;i++) g[i]=ttb[i];
    update(n,m);
    node X=pren[n],Y=prem[m];
    if(f[X.mi]>=g[Y.mi] || f[X.mx]>=g[Y.mx]) return 0;
    return check1(X.mi,Y.mx,n,m) && check2(X.mi,Y.mx,n,m);
}
signed main(){
int c,n,m;
    cin>>c>>n>>m;
    int T;
    cin>>T;
    for(int i=1;i<=n;i++){
        cin>>a[i];
    }
    for(int i=1;i<=m;i++){
        cin>>b[i];
    }
    cout<<"01"[solve(a,b,n,m)||solve(b,a,m,n)];
    while(T--){
        for(int i=1;i<=n;i++) ta[i]=a[i];
        for(int i=1;i<=m;i++) tb[i]=b[i];
        int k1,k2;
        cin>>k1>>k2;
        for(int i=1,x,y;i<=k1;i++){
            cin>>x>>y;
            ta[x]=y;
        }
        for(int i=1,x,y;i<=k2;i++){
            cin>>x>>y;
            tb[x]=y;
        }
        cout<<"01"[solve(ta,tb,n,m)||solve(tb,ta,m,n)];
    }
    return 0;
}

ABC380G Another Shuffle Window

这么会证明

给你一个长为 \(n\) 的排列 \(p\),随机选择一个长为 \(k\) 的区间随机重排,求逆序对数量的期望。

\(k\le n\le 2\times 10^5\)

诈骗题 + 妙妙题。对于一个无重复元素的序列重排,期望逆序对数为 \(\frac{\binom{n}{2}}{2}\)

证明:考虑对于一个数对 \((i,j)(i<j)\) 是逆序对的概率为 \(\frac{1}{2}\),数对数量为 \(\binom{n}{2}\),证毕。

所以先求出全局的逆序对数,然后枚举每个窗口,则期望逆序对数为 \(\sum(全局逆序对数 - 窗口内逆序对数(重排了没影响)+ 窗口期望逆序对数)/(n-k+1)\)

区间逆序对数可以维护一个树状数组,删掉开头相当于减掉窗口中小于它的数的数量。加上结尾相当于加上区间中大于它的数的数量。总时间复杂度 \(O(n\log n)\)

title: code
collapse: close
```cpp
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int maxn=2e5+7;
const int mod=998244353;
int n,k;
int a[maxn],sum;
int qpow(int a,int b){
    int res=1;
    for(;b;b>>=1,a=a*a%mod) if(b&1) res=res*a%mod;
    return res;
}
int tr[maxn];
#define lowbit(x) (x&-x)
void add(int x,int c){for(int i=x;i<=n;i+=lowbit(i)) tr[i]+=c;}
int query(int x){
    int res=0;
    for(int i=x;i;i-=lowbit(i)) res+=tr[i];
    return res;
}
int quer1(int x){
    int res=query(n)-query(x-1);
    return res;
}
signed main(){
    cin>>n>>k;
    for(int i=1;i<=n;i++) cin>>a[i];
    for(int i=n;i;i--){
        sum+=query(a[i]);
        add(a[i],1);
    }
    for(int i=n;i;i--) tr[i]=0;
    for(int i=1;i<=n;i++){
        sum+=quer1(a[i]);
        add(a[i],1);
    }
    for(int i=n;i;i--) tr[i]=0;
    int E=0,p2=qpow(2,mod-2),pn=qpow(n-k+1,mod-2),now=sum;
    for(int i=1;i<=k;i++) now-=2*quer1(a[i]),add(a[i],1);
    for(int i=1;i<=n-k+1;i++){
        E=(E+pn*(now%mod+k*(k-1)%mod*p2%mod)%mod)%mod;
        now+=2*query(a[i]-1); add(a[i],-1);
         if(i+k<=n) now-=2*quer1(a[i+k]),add(a[i+k],1);
    }
    cout<<E*p2%mod;
    return 0;
}

ABC348G Max (Sum - Max)

分治真玄学真不会

给出 \(N\),以及 \(A_{1...N},B_{1...N}\)

对于每个 \(k\in [1,N]\),找出一个 \(1...N\) 的集合 \(S\),满足 \(|S|=k\)\(\sum\limits_{i\in S}A_i-\max\limits_{i\in S}B_i\) 最大,输出这个值。

\(1\le N\le 2\times 10^5,\space |A_i|\le 10^9,\space |B_i|\le 2\times 10^{14}\)

考虑暴力怎么做。按 \(B_i\) 从小到大排序,对于每个 \(k\),枚举 \(B_i\),选在 \([1,i]\) 上前 \(k\) 大的 \(A_i\) 和,这个主席树维护。时间复杂度 \(O(n^2\log n)\)

现在需要发现一个性质:我们称取到最大值的位置 \(p\)\(k\)决策点,随着 \(k\) 的增大,可以发现这个 \(p\) 是逐渐增大的(选前 \(p\) 大一定比选前 \(p-1\) 大优,且可选范围变大一个)。即 \(k\) 的决策点一定小于等于 \(k+1\) 的决策点。

考虑分治。\(solve(l,r,L,R)\) 表示 \(k\in[l,r],p\in[L,R]\) 的答案。每次跑 \(mid\) 的答案,接着分治到 \(solve(l,mid-1,L,p)\)\(solve(mid+1,r,p,R)\) 两个问题。遍历决策点和 \(k\) 都是 \(O(n)\) 的。所以总复杂度 \(O(n\log n)\)

title: code
collapse: close

```cpp
#include<bits/stdc++.h>
#define int long long
#define ls t[now].lc
#define rs t[now].rc
#define mid ((l+r)>>1)
using namespace std;
const int maxn=2e5+7;
namespace io {
	#define gc() (iS == iT ? (iT = (iS = ibuf) + fread (ibuf, 1, SIZE, stdin), (iS == iT ? EOF : *iS ++)) : *iS ++)
	const int SIZE = (1 << 25) + 1;char ibuf[SIZE], *iS, *iT;char obuf[SIZE], *oS = obuf, *oT = oS + SIZE - 1, c, qu[64];int f, qr;inline void flush () {fwrite (obuf, 1, oS - obuf, stdout);oS = obuf;}inline void putc (char x) {*oS ++ = x;if (oS == oT) flush ();}
    template <class I> inline void read (I &x) {for (f = 1, c = gc(); c < '0' || c > '9'; c = gc()) if (c == '-') f = -1;for (x = 0; c <= '9' && c >= '0'; c = gc()) x = x * 10 + (c & 15); x = f == -1 ? -x : x;}
	template <class I> inline void print (I x) {if (! x) putc ('0'); if (x < 0) putc ('-'), x = -x;while (x) qu[ ++ qr] = x % 10 + '0',  x /= 10;while (qr) putc (qu[qr -- ]);}
	template <class I> inline void println (I x, char c='\n') {print(x); putc(c);}struct Flusher_ {~Flusher_(){flush();}}io_flusher_;
}
using io :: read,io :: putc,io :: print,io :: println;
int n;
struct node{
    int a,b;
    bool operator<(const node &o)const{
        if(b==o.b) return a<o.a;
        return b<o.b;
    }
}a[maxn];
int ans[maxn];
struct hujingtao{
    struct tree{
        int siz,sum;
        int lc,rc;
    }t[maxn<<7];
    int root[maxn];
    int nodecnt;
    void pushup(int now){
        t[now].siz=t[ls].siz+t[rs].siz;
        t[now].sum=t[ls].sum+t[rs].sum;
    }
    void modify(int rt,int &now,int l,int r,int x){
        now=++nodecnt;
        t[now]=t[rt];
        if(l==r){
            t[now].siz++;
            t[now].sum+=l;
            return ;
        }
        if(x<=mid){
            modify(t[rt].lc,ls,l,mid,x);
        }else{
            modify(t[rt].rc,rs,mid+1,r,x);
        }
        pushup(now);
    }
    int query(int now,int l,int r,int x){
        if(!x) return 0;
        if(l==r) return x*l;
        if(x<=t[rs].siz) return query(rs,mid+1,r,x);
        else return t[rs].sum+query(ls,l,mid,x-t[rs].siz);
    }
}tr;
void solve(int l,int r,int L,int R){ // 当前区间/决策点区间
    if(l>r) return;
    int ll=max(L,mid),kpos=-1;
    ans[mid]=-1e18;
    for(int i=ll;i<=R;i++){
        int res=tr.query(tr.root[i],-1e9,1e9,mid);
        if(ans[mid]<res-a[i].b) kpos=i,ans[mid]=res-a[i].b;
    }
    solve(l,mid-1,L,kpos); solve(mid+1,r,kpos,R);
}
signed main(){
    read(n);
    for(int i=1;i<=n;i++){
        read(a[i].a); read(a[i].b);
    }
    sort(a+1,a+n+1);
    for(int i=1;i<=n;i++){
        tr.modify(tr.root[i-1],tr.root[i],-1e9,1e9,a[i].a);
    }
    solve(1,n,1,n);
    for(int i=1;i<=n;i++){
        cout<<ans[i]<<'\n';     
    }
    return 0;
}

P5305 旧词 + P4211 LCA

再见离线差分拆贡献

给一棵 \(n\) 个点的树,\(q\) 次询问给出 \(l,r,x\),求

\[\sum\limits_{i=l}^r [dep(lca(i,x))]^k \]

\(k\le 10^9,n,q\le 10^5\)

先考虑 \(k=1\) 的情况。

这题第一步就比较难想,同时也是一个经典转化。LCA 的深度,可以转化为两个点到根路径上重合的点数。

暴力做就是 \([l,r]\) 到根的链加 1,查 \(x\) 到根的链和,这个可以树剖做到 \(O(qn^2\log n)\)

但是考虑到只有加法。于是差分,答案为 \([1,r]\) 的链和减 \([1,l-1]\) 的链和,把询问离线下来扫描线,在加了 \(l-1\) 之后查一次权为 -1 的链和,加 \(r\) 之后查一次权为 1 的链和。时间复杂度 \(O((n+q)\log^2 n)\)。至此我们解决了 \(k=1\) 的问题。

\(k\neq 1\) 怎么搞?

延续前面的思路,考虑差分构造点权。由于我们统计时的链肯定对于每个链加都是从根开始的一段前缀,我们构造 \(a_u=dep(u)^k-(dep(u)-1)^k\) 这样到根的时候刚好抵消完。具体做法就是对每个线段树节点赋权即可。时间复杂度不变。

title: code
collapse: close
```cpp
#include<bits/stdc++.h>
#define int long long
#define ls (now<<1)
#define rs (now<<1|1)
#define mid ((l+r)>>1)
using namespace std;
const int maxn=5e4+7;
const int mod=998244353;
int n,m,k,fa[maxn],dep[maxn],top[maxn],dfn[maxn],idx[maxn],dfncnt,siz[maxn],son[maxn];
vector<int>e[maxn];
void dfs1(int u){
    dep[u]=dep[fa[u]]+1;
    siz[u]=1;
    for(int v:e[u]){
        if(v!=fa[u]){
            dfs1(v);
            siz[u]+=siz[v];
            if(siz[son[u]]<siz[v]) son[u]=v;
        }
    }
}
void dfs2(int u,int t){
    top[u]=t;
    dfn[u]=++dfncnt;
    idx[dfncnt]=u;
    if(son[u]) dfs2(son[u],t);
    for(int v:e[u])
        if(v!=fa[u]&&v!=son[u])
            dfs2(v,v);
}
int sum[maxn<<3],tag[maxn<<3],val[maxn],pre[maxn];
void pushup(int now){sum[now]=(sum[ls]+sum[rs])%mod;}
void pushdown(int now,int l,int r){
    tag[ls]+=tag[now];
    tag[rs]+=tag[now];
    sum[ls]+=tag[now]*(pre[mid]-pre[l-1]+mod)%mod; sum[ls]%=mod;
    sum[rs]+=tag[now]*(pre[r]-pre[mid]+mod)%mod; sum[rs]%=mod;
    tag[now]=0;
}
void modify(int now,int l,int r,int L,int R,int x){
    if(L<=l&&r<=R){
        sum[now]+=x*(pre[r]-pre[l-1]+mod)%mod; sum[now]%=mod;
        tag[now]+=x;
        return;
    }
    pushdown(now,l,r);
    if(L<=mid) modify(ls,l,mid,L,R,x);
    if(mid+1<=R) modify(rs,mid+1,r,L,R,x);
    pushup(now);
}
int query(int now,int l,int r,int L,int R){
    if(L<=l&&r<=R) return sum[now];
    pushdown(now,l,r); int res=0;
    if(L<=mid) res+=query(ls,l,mid,L,R),res%=mod;
    if(mid+1<=R) res+=query(rs,mid+1,r,L,R),res%=mod;
    return res;
}
void modify_tr(int u,int v,int val){
    while(top[u]!=top[v]){
        if(dep[top[u]]<dep[top[v]]) swap(u,v);
        modify(1,1,n,dfn[top[u]],dfn[u],val);
        u=fa[top[u]];
    }
    if(dep[u]>dep[v]) swap(u,v);
    modify(1,1,n,dfn[u],dfn[v],val);
}
int query_tr(int u,int v){
    int res=0;
    while(top[u]!=top[v]){
        if(dep[top[u]]<dep[top[v]]) swap(u,v);
        res+=query(1,1,n,dfn[top[u]],dfn[u]),res%=mod;
        u=fa[top[u]];
    }
    if(dep[u]>dep[v]) swap(u,v);
    res+=query(1,1,n,dfn[u],dfn[v]),res%=mod;
    return res;
}
int qpow(int a,int b){
    int res=1;
    for(;b;b>>=1,a=a*a%mod) if(b&1) res=res*a%mod;
    return res;
}
struct que{
    int id,z,i,v;
    bool operator<(const que &o)const{return i<o.i;}
}q[maxn<<1];
int qcnt;
int ans[maxn];
signed main(){
    ios::sync_with_stdio(0);
    cin.tie(0); cout.tie(0);
    cin>>n>>m>>k; 
    for(int i=2;i<=n;i++){
        cin>>fa[i];
        e[i].push_back(fa[i]);
        e[fa[i]].push_back(i);
    }
    for(int i=1,r,z;i<=m;i++){
        cin>>r>>z; 
        q[++qcnt]={i,z,r,1};
    }
    dfs1(1); dfs2(1,1);
    for(int i=1;i<=n;i++) val[dfn[i]]=(qpow(dep[i],k)-qpow(dep[i]-1,k)+mod)%mod;
    for(int i=1;i<=n;i++) pre[i]=(pre[i-1]+val[i])%mod;
    sort(q+1,q+qcnt+1);
    int pos=1;
    for(int i=1;i<=n;i++){
        modify_tr(1,i,1);
        for(;pos<=qcnt&&q[pos].i==i;pos++) 
            ans[q[pos].id]+=query_tr(1,q[pos].z)*q[pos].v;
    }
    for(int i=1;i<=m;i++) cout<<ans[i]<<'\n';
    return 0;
}

P4254 Blue Mary 开公司

李超树,LiChaoTree,简称 LCT

\(n\) 次操作:

  • 加入一条直线满足 \(f(1)=S,k=P\)
  • \(x=T\) 时所有直线中的最大函数值。

\(n\le 10^5,T\le 5\times 10^4\)

你发现这就是李超线段树板子,然后做完了,加入的直线为 \(f(x)=Px+S-P\)

李超线段树维护区间线段/直线。分别为 \(O(\log^2 n),O(\log n)\)
维护线段要劣些是因为要分成 \(\log n\) 个线段树区间分别处理。
如何维护?现在假设维护 \(x=k\) 时函数值的 max。假设现在是一个被新加入线段 \(u\) 完全覆盖的区间 \([l,r]\)。当前区间也有一个老线段 \(v\),以中点 \(mid\) 的函数值为依据,分为三类:

  • \(u(mid)>v(mid)\):考虑交换 \(u,v\),剩下的情况就是 \(u\) 在中点不如 \(v\) 优的情况了。
  • \(u(mid)\le v(mid)\):这样 \(u\) 能更新答案的区间就仅在一边,往端点(\(l/r\))函数值大于 \(v\) 的方向递归下传即可。

没了。注意精度。

title: code
collapse: close
```cpp
#include<bits/stdc++.h>
#define ls (now<<1)
#define rs (now<<1|1)
#define mid ((l+r)>>1)
using namespace std;
const int maxn=1e5+7;
const int V=5e4;
const double eps=1e-9;
int n,z;
char s[10];
double x,y;
struct line{
    double k,b;
    double operator()(const int x){return k*x+b;}
}a[maxn];
int lcnt,tag[maxn<<2];
int cmp(double x,double y){
    if(x-y>eps) return 1;
    if(y-x>eps) return -1;
    return 0;
}
void update(int now,int l,int r,int u){
    int &v=tag[now];
    int cmid=cmp(a[u](mid),a[v](mid));
    if(cmid==1) swap(u,v);
    if(l==r) return;
    int cl=cmp(a[u](l),a[v](l)),cr=cmp(a[u](r),a[v](r));
    if(cl==1) update(ls,l,mid,u);
    if(cr==1) update(rs,mid+1,r,u);
}
void chmax(double &x,double y){if(cmp(x,y)==-1) x=y;}
int mymax(double x,double y){if(cmp(x,y)==-1) return y; return x;}
double query(int now,int l,int r,int x){
    if(l>x || r<x) return -5e9;
    int res=a[tag[now]](x);
    if(l==r) return res;
    return mymax(res,mymax(query(ls,l,mid,x),query(rs,mid+1,r,x)));
}
signed main(){
    ios::sync_with_stdio(0);
    cin.tie(0); cout.tie(0);
    cin>>n;
    while(n--){
        cin>>s;
        if(s[0]=='P'){
            cin>>x>>y;
            a[++lcnt]={y,x-y};
            update(1,1,V,lcnt);
        }else{
            cin>>z;
            cout<<(int)(query(1,1,V,z)/100)<<'\n';
        }
    }
    return 0;
}

P9871 [NOIP2023] 天天爱打卡

bro 喜欢离散化 😓

有一个长为 \(n\) 的序列 \(a\),初始全为 0。将一个位置置为 1 需要花费 \(d\) 分,有 \(m\) 个奖励 \((l_i,r_i,v_i)\) 表示在 \([l_i,r_i]\) 中全部为 1 可以获得 \(v_i\) 分。且不能有长度大于 \(k\) 的连续 1 段。求可以达到的最大分数。

  • 16 pts:\(n\le 10^2\)
  • 36 pts:\(n\le 10^3\)
  • 56 pts:\(n\le 10^5\)

\(k\le n\le 10^9,m\le 10^5,T\le 10\)

看着就很 DP。考虑一个复杂度关于 \(n\) 的线性 DP。

长度不能超过 \(k\),便枚举断点,设 \(f(i)\) 表示 \(a_i=0\) 的最大分数。考虑枚举 \(j<i\)\((j,i)\) 这一段都是 1,则有转移:

\[f(i)=\max(f(i-1),\max\limits_{j\in[i-k-1,i-2]} (f(j)+val(j,i)-(i-j-1)d)) \]

其中 \(f(0)=0\)\(val(j,i)\) 表示 \((j,i)\) 中包含的奖励区间的分数和。

得到 16pts。

先拆一下式子

\[f(i)=\max(f(i-1),\max\limits_{j\in[i-k-1,i-2]} (f(j)+val(j,i)+jd)-id+d) \]

这个形式比较能线段树了。但是 \(val\) 怎么搞?

用扫描线的思想。当当前 \(i\) 存在一个区间的右端点 \(r_j\)\(i\) 及以后的点才有可能吃到区间 \([l_j,r_j]\) 的分。再考虑我们设的状态,当我们从 \(f(t)(t<l_j)\) 的位置转移过来时,我们才能得到分。由于之前的 DP 值已经确定,所以我们可以线段树维护之前的 \(f\),区间加,遇到一个 \(r_j\) 就把 \(f(0\sim l_j-1)\) 加上 \(v_j\)

所以我们做到了 \(O(n\log n)\),线段树维护区间 max,区间加,得到 56pts。

由于必须在区间 \([l_i,r_i]\) 全为 1 才能吃到分,所以发现有些状态是无用/等价的:

  • \([l_i-1,r_j+1]\)\(l_i\)\(r_j\) 离最近的端点:要么全都选,要么全不选。

实际上就是端点值有用。

考虑把所有端点存下来离散化,然后转移即可。时间复杂度 \(O(m\log m)\)

title: code
collapse: close

```cpp
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int maxn=2e5+7;
const int inf=0x3f3f3f3f3f3f3f3f;
int n,m,k,d;
struct note{
    int l,r,v;
}a[maxn];
struct par{
    int l,v;
};
vector<par>q[maxn];
#define ls (now<<1)
#define rs (now<<1|1)
#define mid ((l+r)>>1)
struct node{
    int sum;
    node(int sum=0): sum(sum){}
    node friend operator+(const node &a, const node &b){
        node c;
        c.sum=max(a.sum,b.sum);
        return c;
    }
}tr[maxn<<2];
int tag[maxn<<2];
void upd(node &x,node y){x=x+y;}
void pushup(int now){
    tr[now]=tr[ls]+tr[rs];
}
void pushdown(int now){
    tag[ls]+=tag[now]; tag[rs]+=tag[now];
    tr[ls].sum+=tag[now]; tr[rs].sum+=tag[now];
    tag[now]=0;
}
void modify(int now,int l,int r,int L,int R,int x){
    if(L>R) return;
    if(L<=l&&r<=R){
        tr[now].sum+=x;
        tag[now]+=x;
        return;
    }
    pushdown(now);
    if(L<=mid) modify(ls,l,mid,L,R,x);
    if(mid+1<=R) modify(rs,mid+1,r,L,R,x);
    pushup(now);
}
node query(int now,int l,int r,int L,int R){
    if(L>R) return node();
    if(L<=l&&r<=R){
        return tr[now];
    }
    pushdown(now);
    node res(-inf);
    if(L<=mid) upd(res,query(ls,l,mid,L,R));
    if(mid+1<=R) upd(res,query(rs,mid+1,r,L,R));
    return res;
}
void chmax(int &x,int y){if(x<y) x=y;}
int g; int c[maxn],ccnt,id[maxn];
void solve(){
    cin>>n>>m>>k>>d; g=ccnt=0;
    for(int i=1,x;i<=m;i++){
        cin>>a[i].r>>x>>a[i].v; a[i].l=a[i].r-x+1;
        c[++ccnt]=a[i].l-1; c[++ccnt]=a[i].r+1;
    }
    sort(c+1,c+ccnt+1); ccnt=unique(c+1,c+ccnt+1)-c-1;
    for(int i=1;i<=m;i++){
        int tl=lower_bound(c+1,c+ccnt+1,a[i].l-1)-c,tr=lower_bound(c+1,c+ccnt+1,a[i].r+1)-c;
        q[tr].push_back({tl,a[i].v});
    }
    for(int i=1;i<=ccnt;i++){
        int now=c[i];
        for(auto j:q[i]) modify(1,1,ccnt,1,j.l,j.v);
        int tl=lower_bound(c+1,c+ccnt+1,now-k-1)-c,tr=upper_bound(c+1,c+ccnt+1,now-2)-c-1;
        if(tl<=tr) chmax(g,query(1,1,ccnt,tl,tr).sum-d*(now-1));
        modify(1,1,ccnt,i,i,g+now*d);
    }
    cout<<max(0ll,g)<<'\n';
    for(int i=1;i<=4*ccnt+4;i++) tr[i].sum=0,tag[i]=0;
    for(int i=0;i<=ccnt+1;i++) q[i].clear();
}
signed main(){
    ios::sync_with_stdio(0);
    cin.tie(0); cout.tie(0);
    int T,c;
    cin>>c>>T;
    while(T--){
        solve();
    }
    return 0;
}

P12003 在小小的奶龙山里面挖呀挖呀挖(加强版)

我不是奶龙

给你一棵树,每个节点有一个集合 \(S_i=\{p\in \mathbb{P}\mid p\mid a_i\}\)\(Q\) 组询问给出 \((u,v)\),求 \(u,v\) 路径上的集合的并集大小。

EZ Ver.:\(n,Q\le 5\times 10^4,a_i\le V\le 10^5\)
HD Ver.:\(n,Q\le 3\times 10^5,a_i\le V\le 10^8\)

\(\sqrt V\) 分类讨论对 \(a_i\) 分解。

\(\forall p\le \sqrt V\),先将每个点设为 \(t_i=[p\mid a_i]\),然后计算出从根到 \(i\)\(t_i\)\(s_i\),对于每个询问进行判断,直接差分即可,对于询问 \((u,v)\) 的答案累加上 \([s_u+s_v-2s_{lca(u,v)}+t_{lca(u,v)}>0]\)。这部分时间复杂度 \(O((Q+n)d(\sqrt{V}))\)

显然每个 \(a_i\) 最多有 1 个大于 \(\sqrt V\) 的质因子,所以把这个质因子当做该点的颜色,若没有就标记为无色。这就是树上数颜色的板子,树上莫队即可。时间复杂度 \(O(n\sqrt Q)\)

这样会被 HD 卡掉,考虑人类智慧,按 \(\sqrt[3]{V}\) 分类,这样每个 \(a_i\) 最多有 2 个大于 \(\sqrt[3]{V}\) 的质因子。直接莫队还是没有问题的。前半部分的复杂度降到了 \(O((Q+n)d(\sqrt[3]{V}))\),可以通过。

或者全用莫队,复杂度是 \(O(n\sqrt Q\log n)\) 的,会被卡。你考虑把所有数的质因数按括号序塞到一个 vector 里面,然后记录开头结尾,这样访问内存连续,会快很多,而且把莫队的 add/del 的 if 改成判断,奇偶排序,直接可以过。

至于如何取得最大的两个质因子,有一个非常神秘的 \(O(V+n\log n)\) 的筛法。记 \(pre(x)\)\(x\) 是被哪个质数筛掉的,则一直令 \(x=pre(x)\) 即可。

全莫队写法:

title: code
collapse: close

```cpp
#include<bits/stdc++.h>
using namespace std;
const int maxn=3e5+7;
int B=548;
const int sv=1e8;
bool ST;
int n,m;
int a[maxn];
vector<int>e[maxn];
vector<int>PR;
int st[maxn*2],ed[maxn*2];
int pr[5761455+3],pcnt,pre[sv+3];
bool isp[sv+3],vis[maxn];
int dfn[maxn][2],dfncnt,idx[maxn*2];
int fa[maxn][19],dep[maxn];
int ton[5761455+3],cnt,ans[maxn];
bool ED;
void sieve(){
    isp[1]=1;
    for(int i=2;i<=sv;i++){
        if(!isp[i]) pr[++pcnt]=i,pre[i]=pcnt;
        for(int j=1;j<=pcnt&&pr[j]*i<=sv;j++){
            isp[pr[j]*i]=1;
            pre[pr[j]*i]=j;
            if(i%pr[j]==0) break;
        }
    }
}
inline void dfs(int u,int F){
    fa[u][0]=F; dep[u]=dep[F]+1;
    dfn[u][0]=++dfncnt;
    idx[dfncnt]=u; 
    for(int v:e[u]) if(v!=F) dfs(v,u);
    dfn[u][1]=++dfncnt;
    idx[dfncnt]=u;
}
void init(){
    for(int j=1;j<=18;j++)
        for(int i=1;i<=n;i++)
            fa[i][j]=fa[fa[i][j-1]][j-1];
}
inline int lca(int u,int v){
    if(u==v) return u;
    if(dep[u]<dep[v]) swap(u,v);
    int d=dep[u]-dep[v];
    for(int i=0;i<=18;i++)
        if(d&(1<<i)) u=fa[u][i];
    if(u==v) return u;
    for(int i=18;i>=0;i--)
        if(fa[u][i]!=fa[v][i])
            u=fa[u][i], v=fa[v][i];
    return fa[u][0];
}
struct node{
    int l,r,id,lca;
    inline bool operator<(const node &o)const{
        if(l/B==o.l/B) return l/B&1?r<o.r:r>o.r;
        return l<o.l;
    }
}q[maxn];
inline void add(int x){ for(int i=st[x];i<ed[x];i++) cnt+=!ton[PR[i]]++; }
inline void del(int x){ for(int i=st[x];i<ed[x];i++) cnt-=!--ton[PR[i]]; }
inline void addr(int x){ for(int i=ed[x]-1;i>=st[x];i--) cnt+=!ton[PR[i]]++; }
inline void delr(int x){ for(int i=ed[x]-1;i>=st[x];i--) cnt-=!--ton[PR[i]]; }
inline void calc(int x){ (vis[idx[x]]^=1) ?add(x) :del(x); }
inline void calcr(int x){ (vis[idx[x]]^=1) ?addr(x) :delr(x); }
signed main(){
    ios::sync_with_stdio(0);
    cin.tie(0); cout.tie(0);
    cin>>n>>m; sieve(); B=2*n/sqrt(m);
    for(int i=1;i<=n;i++) cin>>a[i];
    for(int i=1,u,v;i<n;i++){
        cin>>u>>v;
        e[u].push_back(v);
        e[v].push_back(u);
    }
    dfs(1,0); init();
    for(int i=1;i<=dfncnt;i++){
        st[i]=PR.size();
        int t=a[idx[i]];
        while(t!=1){
            int y=pre[t]; PR.push_back(y);
            while(pre[t]==y) t/=pr[y];
        }
        ed[i]=PR.size();
    }
    for(int i=1,u,v;i<=m;i++){
        cin>>u>>v; 
        if(dfn[u][0]>dfn[v][0]) swap(u,v);
        int t=lca(u,v),uu,vv;
        uu=dfn[u][t!=u],vv=dfn[v][0];
        q[i]={uu,vv,i,dfn[t*(t!=u)][0]};
    }
    sort(q+1,q+m+1);
    int l=1,r=0;
    for(int i=1;i<=m;i++){
        while(l>q[i].l) calc(--l);
        while(r<q[i].r) calcr(++r);
        while(l<q[i].l) calc(l++);
        while(r>q[i].r) calcr(r--);
        if(q[i].lca) calcr(q[i].lca); 
        ans[q[i].id]=cnt;
        if(q[i].lca) calcr(q[i].lca); 
    }              
    for(int i=1;i<=m;i++)
        cout<<ans[i]<<'\n';
    return 0;
} 
posted @ 2024-11-04 21:26  view3937  阅读(34)  评论(0)    收藏  举报
Title