[正睿集训2021] 分治和分块

先口胡一下做法,有链接的题目再去写一下代码吧。

四元环计数

这是一个知识点,但是我现在不会

边分治

其实这东西和点分治差不了多少,但是由于分治树是二叉树所以会很有用。

中心思想就是每次找到一个最好的边(两边子树大小相差最小),然后以这个边作为根递归两个子树。类似于 \(\tt kruskal\) 重构树,底层的节点都一定是原树上的节点。

但是会被菊花图卡掉,所以我们先要对原树变形,建立一些虚点让原树的每个节点的度数大小都为 \(3\),具体来说就是加一些虚点把某个点的儿子全部串起来,这个叫做三度化

void dfs(int u,int fa)//三度化
{
	int tmp=u;
	for(int i=g1.f[u];i;i=g1.e[i].next)
	{
		int v=g1.e[i].v,c=g1.e[i].c;
		if(v==fa) continue;
		num++;
		g.add(tmp,num,0);
		g.add(num,v,c);
		tmp=num;//!!!!!! 
		dis[v]=dis[u]+c;
		dfs(v,u);
	}
}

然后有一个误区,就是边分治的边其实不用建出点来,只用保证树的形态就行了。

至于优劣以后再补充

[BZOJ3636] 教义问答手册

题目描述

一个整数序列,给定若干个询问,在 \([l_i,r_i]\) 中选出若干个不相交的长度为 \(L\) 的区间,使其和最大。

\(n,q\leq1e5,L\leq50\)

解法

有一种分治方法我取名为:中点分治 ,就是每次处理过中点的询问。

假设现在的分治区间是 \([l,r]\) ,对于每一个 \(l'\leq mid\) ,我们处理出 \([l',mid]\) 中选若干段长为 \(L\) 的不相交的区间,最后留下了 \(x\) 个数的最大值。对于每一个 \(r'>mid\) ,我们处理出 \((mid,r']\) 中选若干段长为 \(L\) 的不相交的区间,开头留下了 \(x\) 个数的最大值。

考虑如何回答询问,对于一个过中点的询问,枚举 \(x\) 表示 \([l,mid]\) 最后留下的个数,和 \((mid,r]\)\(L-x\) 个数拼起来就行了,预处理用 \(dp\) 很容易实现,时间复杂度 \(O(nL\log n)\)

#include <cstdio>
#include <iostream>
using namespace std;
const int M = 100005;
int read()
{
	int x=0,f=1;char c;
	while((c=getchar())<'0' || c>'9') {if(c=='-') f=-1;}
	while(c>='0' && c<='9') {x=(x<<3)+(x<<1)+(c^48);c=getchar();}
	return x*f;
}
int n,m,L,a[M],ans[M],s[M],f1[55][M],f2[55][M];
struct node
{
	int l,r,id;
}q[M],t1[M],t2[M];
void cdq(int l,int r,int ql,int qr)
{
	if(l==r || ql>qr)
	{
		for(int i=ql;i<=qr;i++)
			if(L==1) ans[q[i].id]=max(0,a[l]);
		return ;
	} 
	int mid=(l+r)>>1;
	for(int x=0;x<=L;x++)
		for(int i=l;i<=r;i++)
			f1[x][i]=f2[x][i]=0;
	for(int x=0;x<L;x++)//预留出来的位置
	{
		for(int i=mid-x;i>=l;i--)
		{
			f1[x][i]=f1[x][i+1];
			if(i+L<=mid-x+1)
				f1[x][i]=max(f1[x][i],f1[x][i+L]+s[i+L-1]-s[i-1]);
		}
		for(int i=mid+x+1;i<=r;i++)
		{
			f2[x][i]=f2[x][i-1];
			if(i-L>=mid+x)
				f2[x][i]=max(f2[x][i],f2[x][i-L]+s[i]-s[i-L]);
		}
	}
	int tl=0,tr=0;
	for(int i=ql;i<=qr;i++)
	{
		if(q[i].l<=mid && q[i].r>mid)
		{
			int t=f1[0][q[i].l]+f2[0][q[i].r];
			for(int x=1;x<L;x++)
				if(mid-L+x+1>=q[i].l && mid+x<=q[i].r)
					t=max(t,s[mid+x]-s[mid-L+x]+f1[L-x][q[i].l]+f2[x][q[i].r]);
			ans[q[i].id]=max(0,t);
		}
		else if(q[i].r<=mid)
			t1[++tl]=q[i];
		else
			t2[++tr]=q[i];
	}
	for(int i=1;i<=tl;i++) q[ql+i-1]=t1[i];
	for(int i=1;i<=tr;i++) q[ql+tl+i-1]=t2[i];
	cdq(l,mid,ql,ql+tl-1);
	cdq(mid+1,r,ql+tl,ql+tl+tr-1);
}
int main()
{
	n=read();L=read();
	for(int i=1;i<=n;i++)
	{
		a[i]=read();
		s[i]=s[i-1]+a[i];
	}
	m=read();
	for(int i=1;i<=m;i++)
	{
		q[i].l=read();q[i].r=read();q[i].id=i;
	}
	cdq(1,n,1,m);
	for(int i=1;i<=m;i++)
		printf("%d\n",ans[i]);
}

CF1100F Ivan and Burgers

题目描述

点此看题

解法

选出一些数使得其异或和最大很容易想到线性基吧,我们只需要构建出区间的线性基就可以得到答案了。

使用中点分治,对于分治区间 \([l,r]\) ,处理出 \([l',mid]\) 的线性基和 \((mid,r']\) 的线性基。线性基是支持合并的,其实就是把一个线性基里的元素暴力插入到另一个,对于每个询问我们暴力合并,时间复杂度 \(O(log^2c)\)

总时间复杂度 \(O(n\log n\log c+q\log^2c)\)

比赛

题目描述

\(n\) 个两两不同的数 \(a_i\) ,现在你要选一个不下降子序列,对于相邻两元素之间的间隔 \(x\) 花费是 \((x+1)^2\) ,求这个子序列最小的花费。

\(n\leq5e5\)

解法

首先可以列出暴力的 \(dp\) 方程:

\[dp[i]=dp[j]+(i-j)^2\space\space\space a[i]\geq a[j] \]

为了满足 \(a[i]\geq a[j]\) 的条件,可以考虑 \(\tt cdq\) 分治。按照 \(a\) 排序,考虑左半边 \(dp\) 值对右半边的贡献,由于加入的下标没有单调性,所以要写一个动态凸包来维护斜率优化。别走啊这是我口胡的做法

为什么这个做法会复杂呢?因为你对 \(\tt cdq\) 分治的理解不够深入。本题转移其实有两个偏序条件:\(i\geq j,a[i]\geq a[j]\)\(\tt cdq\) 分治的作用是去掉其中的一个偏序条件,相当于给问题降维,然后里面套用的排序可以再去掉一个偏序条件,但是并没有要求先去掉的偏序条件是哪一个

如果我们去掉 \(a[i]\geq a[j]\) 的偏序条件,那么排序去掉 \(i\geq j\) 后就可以方便的用普通斜率优化,因为这时候下标就是有序的。那么我们先按 \(a\) 从小到大排序,然后用 \(\tt cdq\) 分治做斜率优化就很方便了。

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

谢特

题目描述

点此看题

解法

虽然可以直接 \(\tt Sam\)\(\tt tire\) 树启发式合并,但是我们要讲一种新算法。

考虑先用后缀数组求出 \(\tt height\) 数组,因为 \(\tt LCP\) 是一个区间的最小值,所以可以用__最小值分治__ 。

每次找到分治区间 \([l,r]\) 的最小值,然后考虑过这个位置 \(x\) 的贡献,但是要保证复杂度是 \(O(\min(x-l,r-x))\) ,这样一个点产生贡献的时候区间长度必减半,所以复杂度是 \(O(n\log n)\)

这道题我们在做分治回溯的时候要维护出区间的 \(\tt tire\) 树,分治的时候把小区间的每个值都在大区间 \(\tt tire\) 树里面查询一下,然后暴力插入,就可以回溯了,本题时间复杂度 \(O(n\log^2n)\)

Factor-free tree

题目描述

有一颗 \(n\) 个节点的树,满足每个点的点权和其祖先点权互质。

给出这棵树的中序遍历,求这棵树,如果无解输出 \(\tt impossible\)

\(n\leq1e6,a_i\le1e7\)

解法

\(f[i][j]\) 表示区间 \([i,j]\) 是否能构成树,这个可以 \(O(n)\) 枚举根,然后分解成两边,可以 \(O(n^3)\) 把这个东西处理出来。

首先要给出一个结论:如果有解,那么对于区间内可能成为根的点任选一个就行了。证明很简单,我们可以直接把这些可能的根都选出来,剩下的部分递归下去就可以了,和这些根的选择顺序无关了,所以我们只需要找到一个根递归下去就行了。

可以用类似最小值分治的方法找根,维护两个指针,分别从 \(l,r\) 出发向中间扫,假设根的位置是 \(x\) ,那么时间消耗显然是 \(O(2\min(x-l,r-x))\) ,现在问题是快速判断一个数和包含它的区间是否互质。可以预处理一个数互质最多到的左端点和右端点,对于每个质因子都算一遍,对含这个质因子的点处理一下就行了。

[WC2010] 重建计划

题目描述

点此看题

解法

看到这个除法的形式就用 \(01\) 分数规划吧,二分一个 \(mid\) ,把所有边权都减去 \(mid\) ,问题变成了求最长合法路径。

路径问题很容易想到点分治,现在考虑统计分治子树间的贡献。枚举当前子树的深度 \(x\) ,可以在前面的子树中选取的深度范围是 \([L-x,U-x]\) ,用单调队列维护最大值来更新答案就行了。

复杂度怎么证明呢?上面做法的复杂度是有点假的,要让复杂度和分治子树的大小挂钩,我们把分治子树按深度排序,首先因为深度不超过点数,然后当前处理的分治子树的深度是目前最大的,所以复杂度不超过分治子树之和,总时间复杂度是严格的 \(O(n\log^2n)\)

#include <cstdio>
#include <vector>
#include <cstring>
#include <iostream>
#include <algorithm>
using namespace std;
const int M = 100005;
#define db double
const db inf = 1e18;
int read()
{
	int x=0,f=1;char c;
	while((c=getchar())<'0' || c>'9') {if(c=='-') f=-1;}
	while(c>='0' && c<='9') {x=(x<<3)+(x<<1)+(c^48);c=getchar();}
	return x*f;
}
int n,k,L,R,rt,ns,nw,h,t,mx[M],siz[M],vis[M],s[M];
db ans,d1[M],d2[M];pair<int,db> id[M];
struct node{int v,w;db c;};vector<node> g[M];
void find(int u,int fa)
{
	siz[u]=1;mx[u]=0;
	for(auto x:g[u]) if(x.v^fa && !vis[x.v])
	{
		find(x.v,u);
		siz[u]+=siz[x.v];
		mx[u]=max(mx[u],siz[x.v]);
	}
	mx[u]=max(mx[u],ns-siz[u]);
	if(mx[u]<mx[rt]) rt=u;
}
void dfs(int u,int fa,int num,db w)
{
	d2[num]=max(d2[num],w);nw=max(nw,num);
	if(num>R) return ;
	for(auto x:g[u]) if(x.v^fa && !vis[x.v])
		dfs(x.v,u,num+1,w+x.c);
}
void add(int x)
{
	while(h<=t && d1[s[t]]<=d1[x]) t--;
	s[++t]=x;
}
int cmp(pair<int,db> x,pair<int,db> y)
{
	return siz[x.first]<siz[y.first];
}
void solve(int u)
{
	vis[u]=1;k=0;int up=0;
	for(auto x:g[u]) if(!vis[x.v])
		id[++k]=make_pair(x.v,x.c);
	sort(id+1,id+1+k,cmp);
	for(int i=1;i<=k;i++)
	{
		int w=id[i].first;h=1;t=0;nw=0;
		dfs(w,0,1,id[i].second);
		for(int j=min(R,up);j>=L;j--) add(j);
		for(int j=1;j<=nw;j++)
		{
			if(L-j>=0 && L-j<=up) add(L-j);
			while(h<=t && s[h]>R-j) h++;
			if(h<=t) ans=max(ans,d2[j]+d1[s[h]]);
		}
		for(int j=1;j<=nw;j++)
			d1[j]=max(d1[j],d2[j]),d2[j]=-inf;
		up=max(up,nw);
	}
	for(int i=1;i<=siz[u];i++) d1[i]=-inf;
	for(auto x:g[u]) if(!vis[x.v])
	{
		ns=siz[x.v];rt=0;
		find(x.v,0);solve(rt);
	}
}
int check(db mid)
{
	for(int i=1;i<=n;i++)
	{
		vis[i]=0;
		for(auto &x:g[i]) x.c=x.w-mid;
	}
	ans=-inf;
	mx[0]=ns=n;rt=0;
	find(1,0);solve(rt);
	return ans>=0;
}
signed main()
{
	n=read();L=read();R=read();
	for(int i=1;i<n;i++)
	{
		int u=read(),v=read(),c=read();
		g[u].push_back(node{v,c,0});
		g[v].push_back(node{u,c,0});
	}
	db l=0,r=1e6,ans=0;int T=33;
	memset(d1,-0x3f,sizeof d1);
	while(T--)
	{
		db mid=(l+r)/2;
		if(check(mid)) ans=mid,l=mid;
		else r=mid;
	}
	printf("%.3f\n",ans);
}

[PA 2019] Podatki drogowe

题目描述

点此看题

解法

不懂

Wind of change

题目描述

点此看题

给定两棵 \(n\) 个点的树,边带权,对于每一个点 \(i\) ,求 \(\min_j(dis_1(i,j)+dis_2(i,j))\) ,也就是最小化两棵树上的距离和。

解法

点分治这个东西往往都是暴力的,不要想多巧妙的东西了。

先把两棵树都构建出点分树,对于点 \(i\) 的第一个点分树的祖先 \(v_1\) 和第二个点分树的祖先 \(v_2\) ,我们暴力处理出状态 \((v_1,v_2,dis_1(i,v_1)+dis_2(i,v_2))\) ,然后找这个状态中的最小值拼起来就行了,因为点分树的祖先个数是 \(O(\log n)\) 的,所以时间复杂度是 \(O(n\log^2n)\) 的。无脑暴力乱过题系列

但要注意上面的写法会爆空间,所以我们搞点小优化。在第一棵树上枚举 \(v_1\),对 \(v_1\) 所有点都放在第二棵树上去枚举它们的 \(v_2\) 即可,方法是一样的只不过空间优化成了 \(O(n\log n)\)

[CTSC2018] 暴力写挂

题目背景

有些同学已经过了三个题了,写四个题对他们太不公平了,所以蒋仲漠你要过六个题

题目描述

点此看题

解法

挺好的一个题,多讲几种方法。

法一

可以在第一棵树上 \(\tt dsu\space on\space tree\),第二棵树上写一个树剖,时间复杂度 \(O(n\log^3 n)\)

法二

可以在第一棵树上边分治,一个子树为黑点另一个为白点,第二棵树上写虚树,时间复杂度 \(O(n\log^2 n)\),若是写基数排序建虚树和 \(\tt st\) 表求 \(\tt lca\)\(O(n\log n)\),这种方法思路较简单但是很难写。

法三

极其神奇的方法,只能说活久见了。

在第一棵树上建立边分树,先考虑怎么计算两个 \(\max(dep(x)+dep(y)-dep(lca))\),一定要注意边分树上是不好处理 \(\tt lca\) 的信息的,但是却可以处理距离,那么我们计算 \(\frac{1}{2}\max(dep(x)+dep(y)+dis(x,y))\) 即可。边分树上的非叶节点代表了一条边,我们记 \(rt\) 表示这条边的某个节点,那么就转化成了在两个子树中分别选 \(x,y\)\(dep(x)+dis(x,rt)+dep(y)+dis(y,rt)\) 最大即可。

我们枚举第二棵树上的 \(lca\),然后做启发式合并。由于某个点和计算答案有关的只有它边分树的祖先,那么我们尝试只保留这一部分信息,也就是我们维护一棵线段树表示只保留这些点到边分树祖先的点形成的树形结构。那么每次启发式合并的时候合并两棵子树的边分树,就是线段树合并,那么答案用上面的方法就不难计算了。

最后证明一下时间复杂度,因为初始时边分树总点数是 \(O(n\log n)\),每次合并都会减少一个点,那么时间复杂度 \(O(n\log n)\)

代码就不难打了吧,有个 \(80\) 分的代码死活调不出来了。终于调出来了!

#include <cstdio>
#include <cstring>
#include <iostream>
using namespace std;
const int M = 800005;
#define ll long long
int read()
{
	int x=0,f=1;char c;
	while((c=getchar())<'0' || c>'9') {if(c=='-') f=-1;}
	while(c>='0' && c<='9') {x=(x<<3)+(x<<1)+(c^48);c=getchar();}
	return x*f;
}
int n,num,cnt,mi,eg,sz,siz[M],del[2*M],rt[M],lt[M];
int ch[16*M][2];ll dis[M],mx[16*M][2],ans;
struct edge
{
	int v,c,next;
};
template<unsigned int M>struct graph
{
	int tot,f[M];edge e[2*M];
	graph() {tot=1;}//important 
	void add(int u,int v,int c)
	{
		e[++tot]=edge{v,c,f[u]},f[u]=tot;
		e[++tot]=edge{u,c,f[v]},f[v]=tot;
	}
};graph<M> g1,g2;graph<M> g;
void dfs(int u,int fa)//三度化
{
	int tmp=u;
	for(int i=g1.f[u];i;i=g1.e[i].next)
	{
		int v=g1.e[i].v,c=g1.e[i].c;
		if(v==fa) continue;
		num++;
		g.add(tmp,num,0);
		g.add(num,v,c);
		tmp=num;//!!!!!! 
		dis[v]=dis[u]+c;
		dfs(v,u);
	}
}
void dfs2(int u,int fa,ll d,int op)//处理初始形态的边分树
{
	if(!op) sz++;
	if(u<=n)//不是虚点就维护树
	{
		int x=++cnt;//新建节点 
		if(!rt[u]) rt[u]=lt[u]=++cnt;//还没有根 
		mx[lt[u]][op]=d+dis[u];//维护最值 
		ch[lt[u]][op]=x;
		lt[u]=x;
	}
	for(int i=g.f[u];i;i=g.e[i].next)
	{
		int v=g.e[i].v,c=g.e[i].c;
		if(v==fa || del[i]) continue;
		dfs2(v,u,d+c,op);
	}
}
void get(int u,int fa)//找到最好的边 
{
	siz[u]=1;//???一开始没打这个 
	for(int i=g.f[u];i;i=g.e[i].next)
	{
		int v=g.e[i].v;
		if(v==fa || del[i]) continue;
		get(v,u);
		siz[u]+=siz[v];
		if(max(siz[v],sz-siz[v])<mi)
			mi=max(siz[v],sz-siz[v]),eg=i;
	}
}
void solve(int x,int s)//边分治 
{
	if(s==1) return ;//如果已经没有边了 
	mi=1e9;eg=0;sz=s;
	get(x,0);
	del[eg]=del[eg^1]=1;//删去这条边
	int u=g.e[eg].v,v=g.e[eg^1].v;//获取这条边的两个端点
	sz=0;//顺便算一下子树大小 
	dfs2(u,v,0,0);dfs2(v,u,g.e[eg].c,1);
	int tmp=s-sz;//一定要先存下来 
	solve(u,sz);solve(v,tmp);
}
int merge(int x,int y,ll d)//边分树合并
{
	if(!x || !y) return x+y;
	ans=max(ans,max(mx[x][0]+mx[y][1],mx[y][0]+mx[x][1])+d);
	mx[x][0]=max(mx[x][0],mx[y][0]);
	mx[x][1]=max(mx[x][1],mx[y][1]);
	ch[x][0]=merge(ch[x][0],ch[y][0],d);
	ch[x][1]=merge(ch[x][1],ch[y][1],d);
	return x;
}
void dfs3(int u,int fa,ll d)//访问第二棵树
{
	ans=max(ans,2*(dis[u]-d));//x=y的情况 
	for(int i=g2.f[u];i;i=g2.e[i].next)
	{
		int v=g2.e[i].v,c=g2.e[i].c;
		if(v==fa) continue;
		dfs3(v,u,d+c);
		rt[u]=merge(rt[u],rt[v],-2*d);
	}
}
signed main()
{
	n=num=read();
	memset(mx,0xc0,sizeof mx);//important 
	for(int i=1;i<n;i++)
	{
		int u=read(),v=read(),c=read();
		g1.add(u,v,c);
	}
	for(int i=1;i<n;i++)
	{
		int u=read(),v=read(),c=read();
		g2.add(u,v,c);
	}
	dfs(1,0);
	solve(1,num);
	ans=-1e18;
	dfs3(1,0,0);
	printf("%lld\n",ans/2);
}

[WC2018] 通道

题目描述

点此看题

一句话题意:给定三棵树,求 \(\max(dis_1(u,v)+dis_2(u,v)+dis_3(u,v))\)

解法

这个思路真是强到离谱啊,谁告诉我是怎么想出来的啊

惯用的思考方法,我们先考虑两棵树怎么做。可以把第一棵树上的点挂到第二棵树上,就连下去一条长度为 \(dep\) 的边,这样在第一棵树上枚举 \(lca\) 就可以算距离和了,而且我们把两棵树上的问题转化到了一棵树上

\(\tt dfs\) 第一棵树,维护子树内的直径,意思是在第二棵树上距离最远的两个点。得到 \(u\) 的子树内直径就考虑合并 \(v\) 的子树内直径,由于一个经典的结论,合并出来直径的点一定是老直径上的点 ,所以可以做到线性合并直径。

再考虑三棵树怎么做,可以用边分治来处理第一棵树上的路径,然后两棵树的方法是对于全局的点跑 \(\tt dfs\),那么现在我们可以对第一棵分治子树上的点跑 \(\tt dfs\) ,也就是说我们在第二棵树上构建虚树,再沿用只有两棵树的方法。

第三棵树上挂的边权需要设置成到边分树根距离\(+\)第二棵树上的深度,由于边分树是二叉树,而且只能是不同分治子树内的点算贡献,我们把这些点分成 \(A\) 组和 \(B\) 组,维护直径要维护 \(A-A,A-B,B-B\) 三种类型的直径,合并直径的时候好像要讨论 \(36\) 种情况(大雾

sy的分块题

题目描述

维护一个长度为 \(n\) 的正整数序列 \(a\),给定常数 \(m\),需要进行 \(q\) 次操作,有这三种类型:

  • 1 l r:区间模 \(m\)
  • 2 l r:求 \([l,r]\) 的区间和
  • 3 l r x:区间加 \(x\)

\[n\leq2e5,m\le1e5,q\leq1e4 \]

解法

不会

jzm的分块题

题目描述

定义一个区间 \([l,r]\) 的价值是 \((a[l]....+a[r])/(r-l)\)\(q\) 个询问,求满足 \(L\leq l<r\leq R\) 的最大价值。

\(n\leq 100000,q\leq 30000\)

解法

不要看到就直接搞 \(01\) 分数规划了,其实还可以从斜率的角度解读这个算式,定义一类点为 \((i,sum[i-1])\),二类点为 \((i,sum[i])\),问题就转化成了选一个一类点和它后面的二类点,要求斜率最大。

直接上分块,对于询问 \([L,R]\) 那两个点有这样几种情况:

  • 在同一个块内,可以暴力预处理,跑凸包就行了。
  • 同在左边零散块\(/\)右边零散块\(/\)一个在左一个在右,也可以暴力凸包。
  • 一个点在整块中,另一个点不确定。

现在就第三种情况不是很会了,我们考虑构建出整块的凸包,对于一类点我们维护下凸包,扫在它后面的二类点,在上面二分就可以求解。对于二类点我们维护上凸包,扫在它前面的一类点,同样的二分。

我们通过预处理解决了第三种情况,时间复杂度 \(O(n\sqrt n\log n)\)

区间众数

题目描述

给定一个序列,\(q\) 次询问区间,求区间众数。

\(n,q\leq 3e5\)

解法

会了,有时间再写

posted @ 2021-02-16 16:33  C202044zxy  阅读(153)  评论(0编辑  收藏  举报