「分治分块 2022/02 」学习记录
[AGC028B] Removing Blocks
给定数组 \(a_1,a_2,...,a_n\) 现在要求将n个元素全部删除。删除元素 \(a_i\) 时,假设当前包含 \(a_i\) 的极长未被删除序列为 \(a_l,...,a_r\),则代价为 \(\sum\limits_{i=l}^r a_i\)
求所有删除顺序的代价之和。
其中 \(n \le 10^5\) 。
首先对于每种情况的概率都是一样的,所以考虑把题目转为求一种情况的期望然后乘上 \(n!\) 。
每次的操作都是删掉一个点,然后序列变成了两部分,这个过程很笛卡尔树。于是这个点的贡献变成了子树和。
于是每个点的贡献可以变成 \(a_i \times h_i\) ,其中 \(h_i\) 是深度。
对于点 \(i\) ,求出期望祖先点,假设祖先点是 \(x\),如果 \(x<i\) ,那么意味着 \(x+1,x+2,...,i\) 在 \(x\) 后删,期望是 \(\dfrac{1}{i-x+1}\) ;如果 \(x>i\) ,同理,期望 \(\dfrac{1}{x-i+1}\) 。
于是 \(h_i\) 的期望是 \(\dfrac{1}{i-x+1}+\dfrac{1}{x-i+1}+1\) 。预处理逆元然后前缀和即可。
#include<iostream>
#include<cstdio>
#include<cmath>
#include<cstring>
using namespace std;
typedef long long ll;
const ll N=1e5+5;
const ll Mod=1e9+7;
ll n,ans,a[N],inv[N],sum[N];
int main()
{ scanf("%lld",&n);
for(ll i=1;i<=n;i++)
scanf("%lld",&a[i]);
inv[1]=1;
for(ll i=2;i<=n;i++)inv[i]=(Mod-Mod/i)*inv[Mod%i]%Mod;
for(ll i=1;i<=n;i++)sum[i]=(sum[i-1]+inv[i])%Mod;
for(ll i=1;i<=n;i++)ans=(ans+(sum[i]+sum[n-i+1]-1)*a[i]%Mod)%Mod;
for(ll i=1;i<=n;i++)ans=ans*i%Mod;
printf("%lld\n",ans);
return 0;
}
CF232E Quick Tortoise
给定一个 \(n \times m\)的网格图,有一些点是障碍点,不可以经过。
有 \(q\)组询问,给定起点 \((x_1,y_1)\) 和终点 \((x_2,y_2)\),询问能否只通过向右和向下从起点到达终点。
其中 \(n,m \le 500,q \le 6 \times 10^5\) 。
考虑对行分治。对于 \((l,r)\) 的 \(mid\) 。来处理会经过这行的询问。
考虑 DP ,设 \(f(i,j)\) ,其中 \(i<mid\),表示从 \((i,j)\) 可以到达 \((mid,x)\) 的点集。设 \(g(i,j)\) ,其中 \(i>mid\),表示从 \((mid,x)\) 可以到达 \((i,j)\) 的点集。
于是对于询问 \((x_1,y_1,x_2,y_2)\) ,我们只需要看 \(f(x_1,y_1) \cap g(x_2,y_2)\) 是否是空集。
用 bitset
优化。
#include<iostream>
#include<cstdio>
#include<cmath>
#include<cstring>
#include<bitset>
using namespace std;
const int N=500+5,M=6e5+5;
struct query{
int sx,sy,tx,ty,id;
}q[M],t[M];
int n,m,qq,ans[M];
char s[N][N];
bitset<N>f[N][N],g[N][N];
void solve(int l,int r,int ql,int qr)
{ if(l>r||ql>qr)return;
int mid=(l+r)>>1;
for(int i=1;i<=m;i++)f[mid][i]=g[mid][i]=0;
for(int i=m;i>=1;i--)
if(s[mid][i]=='.')f[mid][i][i]=1,f[mid][i]|=f[mid][i+1];
for(int i=1;i<=m;i++)
if(s[mid][i]=='.')g[mid][i][i]=1,g[mid][i]|=g[mid][i-1];
for(int i=mid-1;i>=l;i--)
for(int j=m;j>=1;j--)
{ if(s[i][j]=='.')f[i][j]=f[i+1][j]|f[i][j+1];
else f[i][j]=0;
}
for(int i=mid+1;i<=r;i++)
for(int j=1;j<=m;j++)
{ if(s[i][j]=='.')g[i][j]=g[i-1][j]|g[i][j-1];
else g[i][j]=0;
}
int tl=ql-1,tr=qr+1;
for(int i=ql;i<=qr;i++)
{ if(q[i].tx<mid)t[++tl]=q[i];
else if(q[i].sx>mid)t[--tr]=q[i];
else ans[q[i].id]=(f[q[i].sx][q[i].sy]&g[q[i].tx][q[i].ty]).any();
}
for(int i=ql;i<=tl;i++)q[i]=t[i];
for(int i=tr;i<=qr;i++)q[i]=t[i];
solve(l,mid-1,ql,tl);solve(mid+1,r,tr,qr);
}
int main()
{ scanf("%d%d",&n,&m);
for(int i=1;i<=n;i++)
scanf("%s",s[i]+1);
scanf("%d",&qq);
for(int i=1;i<=qq;i++)
{ scanf("%d%d%d%d",&q[i].sx,&q[i].sy,&q[i].tx,&q[i].ty);
q[i].id=i;
}
solve(1,n,1,qq);
for(int i=1;i<=qq;i++)
{ if(ans[i])printf("Yes\n");
else printf("No\n");
}
return 0;
}
CF364E Empty Rectangles
给定一个
01
矩阵,求有多少子矩形的和恰好为 \(k\) 。其中 \(n,m \le 2500,k\le 6\)。
考虑分治,先对行分治,对于行 \((l,r)\) 的 \(mid\) ,算出由这行拓展的,即包含这行的矩阵答案。
对于上下界我们可以枚举,而列的部分,设 \(f(0/1,k)\) 表示向左/右拓展而来然和是 \(k\) 的最远列。
于是贡献就是 \(\sum\limits_{i=1}^k (f(0,i+1)-f(0,i))\times (f(1,n-i)-f(1,n-i+1))\) 。
但这样复杂度仍然无法通过,我们考虑对行和列一起分治。先分治行再分治列。复杂度 \(\mathcal{O}(knm(\log n+\log m))\) 。
#include<iostream>
#include<cstdio>
#include<cstring>
#include<cmath>
using namespace std;
typedef long long ll;
const int N=2500+5,M=10;
int n,m,K,a[N][N],f[2][M];
char s[N][N];ll ans;
int sum(int x,int u,int y,int v){
return a[u][v]-a[u][y]-a[x][v]+a[x][y];
}
void solve(int lx,int rx,int ly,int ry,int opt)
{ if(lx==rx||ly==ry)return;
if(lx+1==rx&&ly+1==ry)
{ if(sum(lx,rx,ly,ry)==K)ans++;
return;
}
if(opt==0)
{ int mid=(ly+ry)>>1;
solve(lx,rx,ly,mid,1),solve(lx,rx,mid,ry,1);
for(int i=lx;i<rx;i++)
{ f[0][0]=f[1][0]=mid;
for(int j=1;j<=K+1;j++)f[0][j]=ly,f[1][j]=ry;
for(int j=i+1;j<=rx;j++)
{ for(int k=1;k<=K+1;k++)
{ while(sum(i,j,f[0][k],mid)>=k)f[0][k]++;
while(sum(i,j,mid,f[1][k])>=k)f[1][k]--;
}
for(int k=0;k<=K;k++)ans+=1ll*(f[0][k]-f[0][k+1])*(f[1][K-k+1]-f[1][K-k]);
}
}
}
else{
int mid=(lx+rx)>>1;
solve(lx,mid,ly,ry,0),solve(mid,rx,ly,ry,0);
for(int i=ly;i<ry;i++)
{ f[0][0]=f[1][0]=mid;
for(int j=1;j<=K+1;j++)f[0][j]=lx,f[1][j]=rx;
for(int j=i+1;j<=ry;j++)
{ for(int k=1;k<=K+1;k++)
{ while(sum(f[0][k],mid,i,j)>=k)f[0][k]++;
while(sum(mid,f[1][k],i,j)>=k)f[1][k]--;
}
for(int k=0;k<=K;k++)ans+=1ll*(f[0][k]-f[0][k+1])*(f[1][K-k+1]-f[1][K-k]);
}
}
}
}
int main()
{ scanf("%d%d%d",&n,&m,&K);
for(int i=1;i<=n;i++)
{ scanf("%s",s[i]+1);
for(int j=1;j<=m;j++)a[i][j]=(s[i][j]=='1');
for(int j=1;j<=m;j++)
a[i][j]+=a[i][j-1]+a[i-1][j]-a[i-1][j-1];
}
solve(0,n,0,m,0);
printf("%lld\n",ans);
return 0;
}
[AGC002D] Stamp Rally
一张连通图,\(q\) 次询问从两个点 \(x\) 和 \(y\) 出发,希望经过的点(不重复)数量等于 \(z\),经过的边最大编号最小是多少。
其中 \(n,m,q \le 10^5\) 。
考虑一个询问,可以二分后并查集做。
多个询问,整体二分,可撤销并查集。
#include<iostream>
#include<cstdio>
#include<cstring>
#include<cmath>
#include<stack>
using namespace std;
const int N=1e5+5;
struct edge{
int u,v;
}e[N<<1];
struct query{
int x,y,z,id;
}q[N],t[N];
int n,m,qq,sz[N],fa[N],ans[N];
stack<edge> st;
int find(int x)
{ if(fa[x]==x)return x;
return find(fa[x]);
}
void solve(int l,int r,int ql,int qr)
{ //printf("%d %d %d %d\n",l,r,ql,qr);
if(l==r)
{ for(int i=ql;i<=qr;i++)ans[q[i].id]=l;
int fx=find(e[l].u),fy=find(e[l].v);
if(sz[fx]>sz[fy])swap(fx,fy);
if(fx!=fy)fa[fx]=fy,sz[fy]+=sz[fx];
return;
}
int mid=(l+r)>>1;
for(int i=l;i<=mid;i++)
{ int fx=find(e[i].u),fy=find(e[i].v);
if(sz[fx]>sz[fy])swap(fx,fy);
if(fx!=fy)
{ fa[fx]=fy,sz[fy]+=sz[fx];
st.push((edge){fx,fy});
}
}
int t1=ql-1,t2=0;
for(int i=ql,siz;i<=qr;i++)
{ int fx=find(q[i].x),fy=find(q[i].y);
if(fx==fy)siz=sz[fx];
else siz=sz[fx]+sz[fy];
if(siz>=q[i].z)q[++t1]=q[i];
else t[++t2]=q[i];
}
for(int i=1;i<=t2;i++)q[t1+i]=t[i];
while(!st.empty())
{ edge ee=st.top();
st.pop();
fa[ee.u]=ee.u;sz[ee.v]-=sz[ee.u];
}
solve(l,mid,ql,t1),solve(mid+1,r,t1+1,qr);
}
int main()
{ scanf("%d%d",&n,&m);
for(int i=1;i<=m;i++)
scanf("%d%d",&e[i].u,&e[i].v);
for(int i=1;i<=n;i++)fa[i]=i,sz[i]=1;
scanf("%d",&qq);
for(int i=1;i<=qq;i++)
{ scanf("%d%d%d",&q[i].x,&q[i].y,&q[i].z);
q[i].id=i;
}
solve(1,m,1,qq);
for(int i=1;i<=qq;i++)
printf("%d\n",ans[i]);
return 0;
}
CF375D Tree and Queries
树上莫队。虽然是以前写的但是就是拿过来了(?我一定有大病
#include<iostream>
#include<cstdio>
#include<cstring>
#include<cmath>
#include<algorithm>
using namespace std;
const int Maxn=1e5+5;
struct edge{
int v,nx;
}e[Maxn<<1];
struct query{
int l,r,k,id;
}q[Maxn];
int n,m,ne,len,num[Maxn],c[Maxn],f[Maxn],sz[Maxn],deep[Maxn];
int tot,dfn[Maxn],idx[Maxn],sum[Maxn],cnt[Maxn],ans[Maxn];
void read(int u,int v)
{ e[++ne].v=v;
e[ne].nx=f[u];
f[u]=ne;
}
void dfs(int u,int ffa)
{ deep[u]=deep[ffa]+1;
sz[u]=1;
idx[u]=++tot;
dfn[tot]=u;
for(int i=f[u];i;i=e[i].nx)
{ int v=e[i].v;
if(v==ffa)continue;
dfs(v,u);
sz[u]+=sz[v];
}
}
bool cmp(query x,query y)
{ if(num[x.l]==num[y.l])return x.r<y.r;
return num[x.l]<num[y.l];
}
void add(int x)
{ cnt[c[x]]++;
sum[cnt[c[x]]]++;
}
void del(int x)
{ sum[cnt[c[x]]]--;
cnt[c[x]]--;
}
void solve()
{ int x=1,y=0,res=0;
for(int i=1;i<=m;i++)
{ int l=q[i].l,r=q[i].r,k=q[i].k;
while(x>l)add(dfn[--x]);
while(x<l)del(dfn[x++]);
while(y>r)del(dfn[y--]);
while(y<r)add(dfn[++y]);
ans[q[i].id]=sum[k];
}
}
int main()
{ scanf("%d%d",&n,&m);
len=sqrt(n);
for(int i=1;i<=n;i++)
scanf("%d",&c[i]);
for(int i=1;i<=n;i++)
num[i]=(i-1)/len+1;
for(int i=1;i<n;i++)
{ int u,v;
scanf("%d%d",&u,&v);
read(u,v);read(v,u);
}
dfs(1,0);
for(int i=1;i<=m;i++)
{ int x,y;
scanf("%d%d",&x,&y);
q[i].l=idx[x];
q[i].r=idx[x]+sz[x]-1;
q[i].k=y;
q[i].id=i;
}
sort(q+1,q+1+m,cmp);
solve();
for(int i=1;i<=m;i++)
printf("%d\n",ans[i]);
return 0;
}
CF13E Holes
有 \(n\) 个洞排成一条直线,第i个洞有力量值 \(a_i\),当一个球掉进洞i时就会被立刻弹到 \(i+a_i\),直到超出 \(n\)。
进行 \(m\) 次操作:
修改第 \(i\) 个洞的力量值 \(a_i\);
在洞 \(x\) 上放一个球,问该球几次后被哪个洞弹飞出界;
其中 \(n,m \le 10^5\) 。
考虑分块,每个点记录跳几次会跳出这个块并且回到哪里。然后直接修改查询即可。
复杂度 \(\mathcal{O}(n \sqrt{n})\) 。
#include<iostream>
#include<cstdio>
#include<cstring>
#include<cmath>
using namespace std;
const int N=2e5+5;
int n,m,len,ans1,ans2,a[N],bl[N],L[N],R[N],cnt[N],pos[N];
void modify(int x,int y)
{ a[x]=y;
for(int i=x;i>=L[bl[x]];i--)
{ if(i+a[i]>R[bl[x]])cnt[i]=1,pos[i]=i+a[i];
else cnt[i]=cnt[i+a[i]]+1,pos[i]=pos[i+a[i]];
}
}
void query(int x)
{ ans1=ans2=0;
for(int i=x;i<=n;i=pos[i])ans1=i,ans2+=cnt[i];
for(int i=ans1;i<=n;i+=a[i])
if(i+a[i]>n)ans1=i;
}
int main()
{ scanf("%d%d",&n,&m);
for(int i=1;i<=n;i++)
scanf("%d",&a[i]);
len=sqrt(n);
for(int i=1;i<=len;i++)L[i]=(n/len)*(i-1)+1,R[i]=(n/len)*i;
R[len]=n;
for(int i=1;i<=len;i++)
for(int j=L[i];j<=R[i];j++)
bl[j]=i;
for(int i=1;i<=len;i++)
for(int j=R[i];j>=L[i];j--)
{ if(j+a[j]>R[i])cnt[j]=1,pos[j]=j+a[j];
else cnt[j]=cnt[j+a[j]]+1,pos[j]=pos[j+a[j]];
}
while(m--)
{ int opt,x,y;
scanf("%d%d",&opt,&x);
if(opt==0)
{ scanf("%d",&y);
modify(x,y);
}
else{
query(x);
printf("%d %d\n",ans1,ans2);
}
}
return 0;
}
CF797E Array Queries
给定长度为 \(n\) 的序列 \(a\),\(q\) 次询问,每次询问给出 \(p,k\),现在不断进行操作 \(p \leftarrow p+a[p]+k\),直到 \(p>n\) 为止,输出操作次数。
其中 \(n,q \le 10^5\) 。
对询问根号分治。如果 \(p \le \sqrt{n}\) 直接预处理出所有 \(p,k\) 情况,然后查询直接查找即可。
如果 \(p \ge \sqrt{n}\) ,直接暴力查找,复杂度是 \(\mathcal{O}\left(\dfrac{q}{\sqrt{n}}\right)\) 。
复杂度 \(\mathcal{O}(n \sqrt{n})\) 。
#include<iostream>
#include<cstdio>
#include<cstring>
#include<cmath>
using namespace std;
const int N=1e5+5,M=400+5;
int n,m,len,a[N],f[N][M];
int main()
{ scanf("%d",&n);
for(int i=1;i<=n;i++)
scanf("%d",&a[i]);
len=sqrt(n);
for(int i=n;i>=1;i--)
for(int j=1;j<=len;j++)
{ if(i+a[i]+j>n)f[i][j]=1;
else f[i][j]=f[i+a[i]+j][j]+1;
}
scanf("%d",&m);
while(m--)
{ int p,k;
scanf("%d%d",&p,&k);
if(k<=len)printf("%d\n",f[p][k]);
else{
int ans=0;
for(int j=p;j<=n;j=j+k+a[j])ans++;
printf("%d\n",ans);
}
}
return 0;
}
CF1613F Tree Coloring
给定 \(n\) 个节点的树,每次往每个节点上放 \(1\sim n\) 不重复的数,要求不存在某个节点,满足其父节点的数比自己大 \(1\) 。求方案数。
其中 \(n \le 2.5 \times 10^5\)。
首先考虑容斥,\(f(i)\) 表示至少有 \(i\) 对点不满足情况。那么答案就是 \(\sum\limits_{i=0}^{n-1}f(i)\)。
因为数是排列,所以一个父节点只有一个子节点比他小 \(1\) ,于是不满足的点之间可以构成若干条链。考虑缩成点,不妨设剩下 \(t\) 个点。
可以发现这 \(t\) 个点的答案的贡献是 \(t!\) 。所以会贡献到 \(f(n-t)\) 。
设 \(g(i)\) 缩了 \(i\) 个点的贡献。于是有 $f(i)=(n-i)! \cdot g(i) $ 。
于是考虑设 \(g(i)=[x^i]\prod\limits_{u=1}^n(d_ux+1)\) 。其中 \(d_u\) 是儿子数,因为对于所有点,都可以选其中一个儿子。如果不选就是 \(1\) 。
所以我们只要求这个多项式的 \(x^i\) 的系数,就是 \(g(i)\) 。
考虑分治 FFT 求,对于 \(s(l,r)\) 先求 \(s(l,mid),s(mid+1,r)\) 。
复杂度 \(\mathcal{O}(n \log^2 n)\) 。
#include<iostream>
#include<cstring>
#include<cstdio>
#include<cmath>
#include<vector>
using namespace std;
typedef long long ll;
const ll N=1e6+5;
const ll Mod=998244353;
const ll G=3,Gi=(Mod+1)/G;
ll n,ans,d[N],mul[N];
ll qpow(ll x,ll k)
{ ll res=1;
while(k)
{ if(k&1)res=res*x%Mod;
x=x*x%Mod;k>>=1;
}
return res;
}
struct poly{
vector<ll>t;
void NTT(ll inv)
{ static ll rev[N];
ll bit=0,tot=t.size();
while((1<<bit)<tot)bit++;
tot=1<<bit;t.resize(tot,0);
for(ll i=0;i<tot;i++)rev[i]=(rev[i>>1]>>1)|((i&1)<<(bit-1));
for(ll i=0;i<tot;i++)
if(i<rev[i])swap(t[i],t[rev[i]]);
for(ll mid=1;mid<tot;mid<<=1)
{ ll w1=qpow((inv==1)?G:Gi,(Mod-1)/(mid<<1));
for(ll i=0;i<tot;i+=(mid<<1))
{ ll wk=1;
for(ll j=0;j<mid;j++,wk=wk*w1%Mod)
{ ll x=t[i+j],y=wk*t[i+j+mid]%Mod;
t[i+j]=(x+y)%Mod,t[i+j+mid]=(x-y+Mod)%Mod;
}
}
}
}
friend poly operator * (poly x,poly y)
{ ll sz=x.t.size()+y.t.size()-1;
x.t.resize(sz,0);y.t.resize(sz,0);
x.NTT(1);y.NTT(1);
for(ll i=0;i<x.t.size();i++)x.t[i]=(x.t[i]*y.t[i])%Mod;
x.NTT(-1);
ll inv=qpow(x.t.size(),Mod-2);
x.t.resize(sz);
for(ll i=0;i<sz;i++)x.t[i]=x.t[i]*inv%Mod;
return x;
}
};
poly getx(int x)
{ poly y;
y.t.push_back(1),y.t.push_back(d[x]);
return y;
}
poly solve(ll l,ll r)
{ if(l==r)return getx(l);
ll mid=(l+r)>>1;
return solve(l,mid)*solve(mid+1,r);
}
int main()
{ mul[0]=mul[1]=1;
scanf("%lld",&n);
for(ll i=2,u,v;i<=n;i++)
{ scanf("%lld%lld",&u,&v);
d[u]++;d[v]++;d[i]--;
mul[i]=mul[i-1]*i%Mod;
}
poly res=solve(1,n);
for(ll i=0;i<n;i++)
ans=(ans+((i&1)?(-1):1)*(mul[n-i]*res.t[i]%Mod)+Mod)%Mod;
printf("%lld\n",ans);
return 0;
}
CSAcademy Round 10 Yury's Tree
给定 \(n\) 个节点的树,点有点权,边有边权。有 \(q\) 次操作 (1) 询问一个点当前的点权 ;(2) 给定 \(u,x,y\),对于所有在 \(u\) 的子树中,且满足 \(u\) 到 \(v\) 路径上所有边权均不小于 \(y\) 的点 \(v\),点权加 \(x\)。
其中 \(n,q \le 10^5\) ,强制在线。
首先每次修改的路径一定是从 \(u\) 出发向下的路径。
考虑点分治,对于一个子树,不妨设原来的深度最小的为 \(rt\) ,目前的重心是 \(x\) 。因为路径必须从上至下并且经过 \(x\) ,于是起点是 \(rt \rightarrow x\) 的路径上,终点是不包括 \(rt\) 子树的其他子树的点。
预处理每个点到每个重心的最短距离,这个是 \(\mathcal{O}(n \log n)\) 级别的,然后排序,修改的时候二分一个后缀,修改用树状数组即可。
一个不知道出处的问题
给定一个 \(n\times m\) 的矩阵,每个有权值 \(a_{i,j}\)。一个网格 \((i,j)\) 对另一网格 \((x,y)\) 的贡献为 \(a_{i,j}\) 除以(两者直线距离 \(+1\) ),并且两网格间距离小于 \(r\)。求 \(k\) 个的格子,使其他格子对他贡献和前 \(k\) 大。
其中 \(n,m \le 5000,r \le 1000\) 。
设 \(d(i,j)\) 表示计算横坐标差 \(i\) 纵坐标差 \(j\) 对其的贡献。如果 \(i+j>r\) 就设为 \(0\) 。
于是对于 \((i,j)\) ,有贡献和 \(\sum\limits_{x,y}a(x,y) \cdot d(x-i,y-j)\) 。
发现这个东西长得很二维卷积。考虑二维卷积,对于 \(h(i,j)=\sum f(x,y) \times g(i-x,j-y)\) ,转化成 \(h(i\cdot n+j)=\sum f(x \cdot n+y) \times g((i-x)n+(j-y))\) 。