好题收集
好题,比较有 trick/套路的题,印象深刻的题
P4240 毒瘤之神的考验
拆式子与根号分治思想的极致融合。
给 \(n,m\),求
对 998244353 取模,多测。
\(n,m\le 10^5,T\le 10^4\)
设 \(n\le m\)。
先要知道一个经典的式子:
然后拆式子
枚举 \(d=\gcd(i,j)\)
后面那个直接莫反拆掉,把枚举扔外面
这个式子就很好看了,设
则原式为
\(f,g\) 直接预处理是可以的,因为 \(g\) 的数量是 \(n\log n\) 级别的,且 \(g\) 可以直接递推:
所以预处理 \(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)\),这个三元函数就可以数论分块了,即
\(h\) 也可以递推
否则可以得到 \(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\),保证有解
由于保证有解,考虑观察有解的情况所带来的性质:
- 区间 \([a_i,i]\) 要么把前面的若干区间完全包含,要么左端点与相邻区间端点相交;
由性质 1 与偏序关系可知,假如以偏序关系(大于号连接的两边)连边,\(p_i\) 为点权,以 \(p_0\) 为根,则形成一棵满足 \(u\) 子树内的点权大于 \(u\) 点权的树。(由不交想到转化为树上问题)
这棵树的性质很好啊,当你用拓扑序遍历这棵树他一定合法,即为充分必要条件了,虽然我没看出来。
接下来就是一个裸的树上拓扑序计数了,也是个结论,即
证明:
考虑树形 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\) 扩展是否合法,则有:
很不能优化的样子。考虑上面在干啥。
相当于有一个矩阵 \(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\),求
\(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(0)=0\),\(val(j,i)\) 表示 \((j,i)\) 中包含的奖励区间的分数和。
得到 16pts。
先拆一下式子
这个形式比较能线段树了。但是 \(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;
}

浙公网安备 33010602011771号