「分治分块 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\) 次操作:

  1. 修改第 \(i\) 个洞的力量值 \(a_i\)

  2. 在洞 \(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))\)


\[\text{by Rainy7} \]

posted @ 2022-02-24 00:50  Rainy7  阅读(119)  评论(0编辑  收藏  举报