根号算法训练记录 2025.2

根号算法其实和暴力很像,但是会按照一定的范围对操作分类讨论或更改顺序,以达到更优的时间复杂度,主要分为根号分治,分块和莫队,通俗来说,就是一种优雅的暴力

根号分治

其实准确来说,根号分治并不是一个分治算法,而是按照数据规模进行预处理和询问的复杂度平衡

对于题目中可以转化为和 xxn 相关的问题,此时,当 x 取到一个特定值 a 时,可以采用两种不同的方式,使得复杂度均等

一般情况下,an

所以,其实根号分治更像一个技巧,或是平常说的数据点分治

Tree Master

https://www.gxyzoj.com/d/hzoj/p/4498

题目中要求的是让两个同深度的点不断向上跳,跟其复杂度相关的第一个量就是整棵树的深度

当深度很大时,每一层的节点数就会很少,可以对每对节点直接预处理

所以可以根据每层节点数进行分治,如果当前层节点数多于 n,就暴力跳,因为至多跳 n

否则记忆化,统计时记录已知的点对,总共至多记 nn 个点

所以总时间复杂度是 O(nn)

点击查看代码
#include<cstdio>
#include<cmath>
#define ll long long
using namespace std;
int n,q,a[100005],f[100005],len;
int id[100005],cnt[100005],dep[100005];
ll mp[100005][320];
ll dfs(int x,int y)
{
	if(x==0&&y==0) return 0;
	if(cnt[dep[x]]<=len)
	{
		if(!mp[x][id[y]])
		mp[x][id[y]]=dfs(f[x],f[y])+1ll*a[x]*a[y];
		return mp[x][id[y]];
	}
	else return dfs(f[x],f[y])+1ll*a[x]*a[y];
}
int main()
{
	scanf("%d%d",&n,&q);
	for(int i=1;i<=n;i++)
	{
		scanf("%d",&a[i]);
	}
	dep[1]=1;
	for(int i=2;i<=n;i++)
	{
		scanf("%d",&f[i]);
		dep[i]=dep[f[i]]+1;
		id[i]=++cnt[dep[i]];
	}
	len=sqrt(n);
	while(q--)
	{
		int x,y;
		scanf("%d%d",&x,&y);
		printf("%lld\n",dfs(x,y));
	}
	return 0;
}

[COTS 2024] 双双决斗 Dvoboj

https://www.gxyzoj.com/d/hzoj/p/LG10680

先考虑不带修的情况,因为是求 2k 个数中剩下的,不难想到 ST 表

如果用 ST 表预处理出所有结果,在不带修的情况下,可以 O(1) 输出,时间复杂度为预处理的 O(nlogn)

接下来加入修改,虽然是单点修改,但是要在ST表上更新,就需要将整个 ST 表重新填,每一次都是 O(nlogn)

如何让更改的涉及范围变小?

注意到,这个 ST 表都要更改,是因为这个单点修改在 k 较大时会影响到几乎所有点,但是在 k 较小时,有意义的修改很少

所以考虑只预处理较小的 k,而超过这个范围的则在询问时再次基础上暴力求解

那么时间复杂度就是 O(nklogk),在 kn 时复杂度就能通过

点击查看代码
#include<cstdio>
#include<cmath>
using namespace std;
int n,q,a[200005],st[200005][20],len,b,t[200005][20];
void build_st()
{
	for(int i=1;i<=n;i++)
	{
		st[i][0]=a[i];
	}
	for(int j=1;(1<<j)<=b;j++)
	{
		for(int i=1;i+(1<<j)-1<=n;i++)
		{
			st[i][j]=abs(st[i][j-1]-st[i+(1<<(j-1))][j-1]);
		}
	}
}
int main()
{
	scanf("%d%d",&n,&q);
	for(int i=1;i<=n;i++)
	{
		scanf("%d",&a[i]);
	}
	b=sqrt(n);
	for(int i=0;i<=20;i++)
	{
		if((1<<i)<=b) len=i;
	}
	build_st();
	while(q--)
	{
		int opt,x,y;
		scanf("%d%d%d",&opt,&x,&y);
		if(opt==1)
		{
			st[x][0]=a[x]=y;
			for(int j=1;(1<<j)<=b;j++)
			{
				for(int i=x-(1<<j)+1;i+(1<<j)-1<=n&&i<=x;i++)
				{
					st[i][j]=abs(st[i][j-1]-st[i+(1<<(j-1))][j-1]);
				}
			}
		}
		else
		{
			if(y<=len)
			{
				printf("%d\n",st[x][y]);
			}
			else
			{
				int tmp=1<<(y-len);
				for(int i=1,j=x;i<=tmp;i++,j+=(1<<len))
				{
					t[i][0]=st[j][len];
				}
				for(int j=1;(1<<j)<=tmp;j++)
				{
					for(int i=1;i+(1<<j)-1<=tmp;i++)
					{
						t[i][j]=abs(t[i][j-1]-t[i+(1<<(j-1))][j-1]);
					}
				}
				printf("%d\n",t[1][y-len]);
			}
		}
	}
	return 0;
}

Till I Collapse

https://www.gxyzoj.com/d/hzoj/p/4501

想到了一部分吧

因为只有 n 个数,所以段数至多是 nk ,而且随着 k 的增大,段数会减小,具有可二分性

所以可以枚举段数为 x 时的最小的 k 然后二分最大的

但是当 k<n 时,这样做是没有意义的,因为范围太小,重复太少,所以可以只在 kn时二分,其余直接求解

点击查看代码
#include<cstdio>
#include<cmath>
using namespace std;
int n,a[100005],cnt[100005];
int get(int x)
{
	int tot=1,sum=0;
	for(int i=1;i<=n;i++) cnt[i]=0;
	for(int i=1;i<=n;i++)
	{
		if(cnt[a[i]]!=tot) cnt[a[i]]=tot,sum++;
		if(sum>x) tot++,sum=1,cnt[a[i]]=tot;
//		printf("%d %d %d %d %d\n",x,a[i],tot,cnt[a[i]]);
//		printf("%d ",tot);
	}
	return tot;
}
int main()
{
	scanf("%d",&n);
	for(int i=1;i<=n;i++)
	{
		scanf("%d",&a[i]);
	}
	int len=sqrt(n);
	for(int i=1;i<=n;i++)
	{
//		printf("%d ",i);
		if(i<=len)
		{
			printf("%d ",get(i));
			continue;
		}
		int l=i,r=n,ans=get(i);
		while(l<r)
		{
			int mid=(l+r+1)>>1;
			if(get(mid)<ans) r=mid-1;
			else l=mid;
		}
//		printf("%d %d\n",i,l);
		for(int j=i;j<=l;j++) printf("%d ",ans);
		i=l;
	}
	return 0;
}

Mr. Kitayuta's Colorful Graph

https://www.gxyzoj.com/d/hzoj/p/4502

看到多少种颜色,联通,考虑将每一种颜色的边分别用并查集缩点,此时,只要枚举 c,然后判断两点是否在同一联通快

但是这样复杂度很高,考虑预处理一些颜色

我们可以提前处理涉及点数小于等于 n 的颜色,这样整个预处理的时间复杂度至多是 O(nn)

此时,至多剩 n 个颜色,询问时间复杂度 O(qn)

注意因为点数很多,要使用map,注意常数

点击查看代码
#include<bits/stdc++.h>
using namespace std;
int n,m,len,f[200005],tot,tp[200005];
map<pair<int,int>,int> mp,id;
vector<int> v,p[100005];
int find(int x)
{
	if(f[x]!=x) f[x]=find(f[x]);
	return f[x];
}
struct edge{
	int x,y,c;
}e[100005];
int read()
{
	int x=0;
	char ch=getchar();
	while(ch<48||ch>57) ch=getchar();
	while(ch>=48&&ch<=57)
	{
		x=x*10+ch-48;
		ch=getchar();
	}
	return x;
}
void write(int x)
{
	if(x>9)
	{
		write(x/10);
	}
	putchar(x%10+48);
	return;
}
int main()
{
	n=read(),m=read();
	int fl;
	for(int i=1;i<=m;i++)
	{
		int x,y,c;
		x=read(),y=read(),c=read();
		pair<int,int> a=make_pair(x,c),b=make_pair(y,c);
		if(!id.count(a))
		{
			p[c].push_back(x);
			id[a]=++tot;
		}
		if(!id.count(b))
		{
			p[c].push_back(y);
			id[b]=++tot;
		}
		e[i]=(edge){x,y,c};
	}
	for(int i=1;i<=tot;i++) f[i]=i;
	for(int i=1;i<=m;i++)
	{
		int x=id[make_pair(e[i].x,e[i].c)];
		int y=id[make_pair(e[i].y,e[i].c)];
		f[find(x)]=find(y);
	}
	len=sqrt(n);
	for(int i=1;i<=m;i++)
	{
		if(!p[i].size()) continue;
		if(p[i].size()<=len)
		{
			for(int j=0;j<p[i].size();j++)
			{
				tp[j]=id[make_pair(p[i][j],i)];
			}
			for(int j=0;j<p[i].size();j++)
			{
				for(int k=j+1;k<p[i].size();k++)
				{
					if(find(tp[j])==find(tp[k]))
					{
						mp[make_pair(p[i][j],p[i][k])]++;
						mp[make_pair(p[i][k],p[i][j])]++;
					}
				}
			}
		}
		else v.push_back(i);
	}
	int q;
	q=read();
	while(q--)
	{
		int x,y;
		x=read(),y=read();
		int ans=0;
		if(mp.count(make_pair(x,y))) ans=mp[make_pair(x,y)];
//		printf("%d\n",ans);
		for(int i=0;i<v.size();i++)
		{
			int c=v[i];
			if(!id.count(make_pair(x,c))) continue;
			if(!id.count(make_pair(y,c))) continue;
			if(find(id[make_pair(x,c)])==find(id[make_pair(y,c)]))
			ans++;
//			printf("%d %d\n",c,ans);
		}
		write(ans);
		putchar('\n');
	}
	return 0;
}

分块

ycz的妹子

https://www.gxyzoj.com/d/hzoj/p/LG4879

其他操作暴力就行了,关键在于 D 操作,分块统计人数,块长设成n即可

[THUPC 2017] 钦妹的玩具商店

https://www.gxyzoj.com/d/hzoj/p/4497

抽象

因为是求每个小朋友的最大愉悦值,一定是要跑背包的。

暴力的一种思路是正着跑一遍,再逆着跑一遍,然后将 l 之前的结果和 r 之后的结果合并。

但是这样的合并是 O(m2) 的。

还有一种暴力的方法是对每一组 [l,r] 重新计算,这样每一问的时间就是 O(nm) 的。

因为合并的时间复杂度太大,必须舍弃,第二种方法每一问重新计算,重复的运算很多。

所以可以预处理出不包含一些 [l,r] 的背包,然后暴力添加剩余块,一定不能计算所有,可以分块。

fi,j,k 表示前 i 块和后 nj+1 块的花费为 k 的最大愉悦值。

转移时,暴力添加多余部分即可,块长可以取n,预处理时间复杂度 O(nmn),询问时间复杂度是 O(qmn)

多重背包部分,可以使用单调优化,二进制优化会多个 log

注意,题目中愉悦度之和是取模后的!

点击查看代码
#include<cstdio>
#include<algorithm>
#include<cmath>
#define ll long long
using namespace std;
const int mod=1e8+7;
int T,n,m,Q,c[1005],lim[1005],blen,t;
int st[40],ed[40],pos[1005],cnt,id[1005];
int head,tail;
ll f[35][35][1005],d[1005],dp[1005],v[1005];
struct node{
	int x;
	ll v;
}q[1005];
void add(int ax,int ay,int bx,int by,int l,int r)
{
//	printf("%d %d %d %d\n",ax,ay,bx,by);
	for(int i=0;i<=m;i++)
	{
		f[ax][ay][i]=f[bx][by][i];
	}
	for(int i=l;i<=r;i++)
	{
		for(int j=0;j<c[i];j++)
		{
			head=1,tail=cnt=0;
			for(int k=j;k<=m;k+=c[i])
			{
				d[++cnt]=f[ax][ay][k];
				id[cnt]=k;
			}
			for(int k=1;k<=cnt;k++)
			{
				while(head<=tail&&q[head].x<k-lim[i]) head++;
				while(head<=tail&&q[tail].v<=d[k]-k*v[i]) tail--;
				q[++tail]=(node){k,d[k]-k*v[i]};
				f[ax][ay][id[k]]=max(f[ax][ay][id[k]],q[head].v+k*v[i]);
			}
		}
	}
}
void add1(int i)
{
	for(int j=0;j<c[i];j++)
	{
		head=1,tail=cnt=0;
		for(int k=j;k<=m;k+=c[i])
		{
			d[++cnt]=dp[k];
			id[cnt]=k;
		}
		for(int k=1;k<=cnt;k++)
		{
			while(head<=tail&&q[head].x<k-lim[i]) head++;
			while(head<=tail&&q[tail].v<=d[k]-k*v[i]) tail--;
			q[++tail]=(node){k,d[k]-k*v[i]};
			dp[id[k]]=max(dp[id[k]],q[head].v+k*v[i]);
		}
	}
}
int main()
{
//	freopen("1.txt","r",stdin);
	scanf("%d",&T);
	while(T--)
	{
		scanf("%d%d%d",&n,&m,&Q);
		for(int i=1;i<=n;i++) scanf("%d",&c[i]);
		for(int i=1;i<=n;i++) scanf("%lld",&v[i]);
		for(int i=1;i<=n;i++) scanf("%d",&lim[i]);
		blen=sqrt(n),t=(n+blen-1)/blen;
		for(int i=1;i<=t;i++)
		{
			st[i]=(i-1)*blen+1,ed[i]=blen*i;
		}
		ed[t]=n;
		for(int i=1;i<=t;i++)
		{
			for(int j=st[i];j<=ed[i];j++) pos[j]=i;
		}
		for(int i=0;i<=t+1;i++)
		{
			for(int j=0;j<=t+1;j++)
			{
				for(int k=0;k<=m;k++)
				{
					f[i][j][k]=0;
				}
			}
		}
		for(int i=t;i>0;i--)
		{
			add(0,i,0,i+1,st[i],ed[i]);
		}
		for(int len=t;len>0;len--)
		{
			for(int i=1;i+len<=t+1;i++)
			{
				int j=i+len;
				add(i,j,i-1,j,st[i],ed[i]);
			}
		}
		ll ans1=0,ans2=0;
		while(Q--)
		{
			ll l,r,fl=0;
			scanf("%lld%lld",&l,&r);
			l=(ans1+l-1)%n+1,r=(ans1+r-1)%n+1;
			if(l>r) swap(l,r);
 			int x=pos[l]-1,y=pos[r]+1;
//			printf("%d %d %d %d\n",l,r,x,y);
			for(int i=0;i<=m;i++)
			{
				dp[i]=f[x][y][i];
			}
			ans1=ans2=0;
			for(int i=st[pos[l]];i<l;i++)
			{
				add1(i);
			}
			for(int i=r+1;i<=ed[pos[r]];i++)
			{
				add1(i);
			}
			ans1=ans2=0;
			for(int i=1;i<=m;i++)
			{
				ans1+=dp[i];
				ans2^=dp[i];
			}
			ans1%=mod;
			printf("%lld %lld\n",ans1,ans2);
		}
	}
	return 0;
}

莫队

Robin Hood Archery

https://www.gxyzoj.com/d/hzoj/p/4504

显然,唯一不会输的情况就是所有数的个数都是偶数,所以莫队统计即可

[SNOI2017]一个简单的询问

https://www.gxyzoj.com/d/hzoj/p/P2013

可以将一个询问拆成四部分也就是 get(1,l11)get(1,l21)get(1,l11)get(1,r2)get(1,r1)get(1,l21)+get(1,r1)get(1,r2)

然后直接莫队处理就行了

XOR and Favorite Number

https://www.gxyzoj.com/d/hzoj/p/4505

可以先做前缀异或和,每次加点的时候加上当前的异或和异或上 k 的结果所对应的点的数量,删点同理

二进制优化

不知道放哪的题(瞎起了个标题)

「SMOI-R1」Apple

https://www.gxyzoj.com/d/hzoj/p/LG10408

卡常题

当不带修的时候,可以用一个高维前缀和,也就是子集dp,但是每次修改都要重新求

可以发现,时间主要消耗在找包含它的集合的时候,因为位数很多,情况很多

可以在更改时先固定前面,处理二进制的后 n2 位,在询问时固定后面,出来前 n2

注意,不要全部枚举,修改时只枚举能够由当前值转移而来的,询问时只枚举能转移到当前值的

时间复杂度 O(q2n2)

点击查看代码
#include<cstdio>
#define ll long long
using namespace std;
int n,q,a[3000005];
ll f[3000005];
int read()
{
	int x=0;
	char ch=getchar();
	while(ch<48||ch>57) ch=getchar();
	while(ch>=48&&ch<=57)
	{
		x=x*10+ch-48;
		ch=getchar();
	}
	return x;
}
void write(ll x)
{
	if(x>9)
	{
		write(x/10);
	}
	putchar(x%10+48);
	return;
}
int main()
{
	n=read(),q=read();
//	printf("%d %d\n",n,q);
	int tmp1=(n+1)/2,tmp2,tx,ty;
	tmp2=n-tmp1;n=(1<<n),tx=(1<<tmp1)-1,ty=((1<<tmp2)-1)<<tmp1;
	for(int i=0;i<n;i++)
	{
		a[i]=read();
		f[i]=a[i];
	}
	for(int j=0;j<tmp1;j++)
	{
		int t1=1<<j;
		for(int i=0;i<n;i++)
		{
			if(t1&i)
			f[i]+=f[i^t1];
		}
	}
	while(q--)
	{
		int opt,s,val;
		opt=read(),s=read();
		if(opt==1)
		{
			int y=s&ty,x=s&tx;
			ll ans=0;
			for(int i=y;i;i=(i-1)&y) ans+=f[i|x];
			ans+=f[s&x];
			write(ans);
			putchar('\n');
		}
		else
		{
			val=read();
			int x=(s&tx)^tx;
			ll tmp=1ll*val-a[s];
			a[s]=val;
			for(int i=x;i;i=(i-1)&x) f[i|s]+=tmp;
			f[s]+=tmp;
		}
	}
	return 0;
}

String Set Queries

https://www.gxyzoj.com/d/hzoj/p/4506

涉及字符串匹配,考虑 AC 自动机,加入的放入一个,删除的放入另一个,求解时相减即可

但是 AC 自动机是静态的,建好后无法加入删除,考虑二进制分组

可以将新加入的点单独作为一组,建 trie,如果当前组和前面组的字符串数量相同,就将 trie 树合并

合并后,在最末的 trie 上建 fail 指针即可

点击查看代码
#include<cstdio>
#include<string>
#include<iostream>
#include<vector>
#include<queue>
using namespace std;
const int N=300005;
int T,tot,now;
struct ACAM{
	int tr[N*2][26],ch[N*2][26],idx,sum[N*2],fail[N*2],val[N*2];
	int rt[N],siz[N],cnt;
	void ins_tr(int x,string s)
	{
		for(int i=0;i<s.size();i++)
		{
			int c=s[i]-'a';
			if(!tr[x][c]) tr[x][c]=++idx;
			x=tr[x][c];
		}
		val[x]++;
	}
	int merge(int x,int y)
	{
		if(!x||!y) return x+y;
		val[x]+=val[y];
		for(int i=0;i<26;i++)
		{
			tr[x][i]=merge(tr[x][i],tr[y][i]);
		}
		return x;
	}
	void getfail(int x)
	{
		queue<int> q;
		fail[x]=x;
		for(int i=0;i<26;i++)
		{
			if(tr[x][i])
			{
				ch[x][i]=tr[x][i];
				q.push(ch[x][i]);
				fail[ch[x][i]]=x;
			}
			else ch[x][i]=x;
		}
		while(!q.empty())
		{
			int u=q.front();
			q.pop();
			for(int i=0;i<26;i++)
			{
				if(tr[u][i])
				{
					ch[u][i]=tr[u][i];
					fail[ch[u][i]]=ch[fail[u]][i];
					q.push(ch[u][i]);
				}
				else ch[u][i]=ch[fail[u]][i];
			}
			sum[u]=val[u]+sum[fail[u]];
		}
	}
	void insert(string s)
	{
		siz[++cnt]=1,rt[cnt]=++idx;
		ins_tr(rt[cnt],s);
		while(siz[cnt]==siz[cnt-1])
		{
			rt[cnt-1]=merge(rt[cnt-1],rt[cnt]);
			siz[cnt-1]*=2,cnt--;
		}
		getfail(rt[cnt]);
	}
	int query(string s)
	{
		int ans=0,p;
		for(int i=1;i<=cnt;i++)
		{
			p=rt[i];
			for(int j=0;j<s.size();j++)
			{
				int c=s[j]-'a';
				p=ch[p][c];
				ans+=sum[p];
			}
		}
		return ans;
	}
}add,del;
int main()
{
	scanf("%d",&T);
	while(T--)
	{
		int opt;
		scanf("%d",&opt);
		string s;
		cin>>s;
		if(opt==1)
		{
			add.insert(s);
		}
		if(opt==2)
		{
			del.insert(s);
		}
		if(opt==3)
		{
			printf("%d\n",add.query(s)-del.query(s));
		}
		fflush(stdout);
	}
	return 0;
}
posted @   wangsiqi2010916  阅读(22)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 无需6万激活码!GitHub神秘组织3小时极速复刻Manus,手把手教你使用OpenManus搭建本
· Manus爆火,是硬核还是营销?
· 终于写完轮子一部分:tcp代理 了,记录一下
· 别再用vector<bool>了!Google高级工程师:这可能是STL最大的设计失误
· 单元测试从入门到精通
点击右上角即可分享
微信分享提示