思想:对于一个问题的不同数据范围,按不同(复杂度)方式解答。通常按根号为界分别。

理解:根号可以将一个不平均的问题,分块+拼接成一个根号级别平均的问题。

例题

缘幂求余

  • 思路:按照快速幂思想\(a^n=(a^2)\)^ \((n/2) *a\),想出了根号级别预处理,O(1)查询的光速幂 \(a^n=(a^k)\) ^ \((n/k)\)*\(a\)^\((n\ mod\ k)\)

  • 代码

#include<stdio.h>
#include<math.h>
using namespace std;
const int N=50005;
long long A[N],B[N];
int main() {
	int x,n,y;
	long long mod=998244352;
	scanf("%d%d",&x,&n);
	long long l=35000;
	A[0]=B[0]=1;
	for(int i=1;i<=l;i++) A[i]=A[i-1]*x%mod;
	for(int i=1;i<=l;i++) B[i]=B[i-1]*A[l]%mod;
	for(int i=1;i<=n;i++) {
		scanf("%d",&y);
		printf("%d ",A[y%l]*B[y/l]%mod);
	}
	return 0;
}

Mod 统计

  • 思路
    当模数\(p>\sqrt{10^5}\)时,可以用方案1:直接从r每次递增p统计和,这样会发现p越大越快,最慢也是\(O(\sqrt{10^5})\)
    否则会发现,我们利用p较小的特点,预处理出sum[p][r]表示%p为r的值的和

  • 代码

#include<bits/stdc++.h>
using namespace std;
const int N=2e5+5;
const int M=1e3+5;
int sum[M][M],a[N];
int main() {
	int n,m,k;
	scanf("%d%d",&n,&m);
	k=(int)(sqrt(n));
	for(int i=1;i<=n;i++) scanf("%d",&a[i]); 
	for(int i=1;i<=k;i++) {
		for(int j=1;j<=n;j++) {
			sum[i][j%i]+=a[j];
		}
	}
	for(int i=1;i<=m;i++) {
		char opt[5];
		int x,y;
		scanf("%s%d%d",&opt,&x,&y);
		if(opt[0]=='A') {
			if(x<=k) printf("%d\n",sum[x][y]);
			else {
				int res=0;
				for(int j=y;j<=n;j+=x) {
					res+=a[j];
				}
				printf("%d\n",res);
			}
		}
		else {
			int tmp=y-a[x];
			for(int j=1;j<=k;j++) sum[j][x%j]+=tmp;
			a[x]=y;
		}
	}
	return 0;
}

子集求和

  • 思路:
    这道题稍难一点qwq,很明显对于一个集合的个数分类:

  • 元素个数\(<\sqrt{n}\),我们可以暴力枚举每个子集

  • 否则,首先m个集合的元素个数总和不超过\(10^5\),因此元素个数\(>\sqrt{n}\)的大集合数不超过\(\sqrt{n}\)个.
    我们预处理出sum[s]表示s子集值的和,复杂度\(O(n*\sqrt{n})\)
    修改s集合时,直接将其Lazy[s]+=val;
    但是注意!!有些小集合和大集合会有交集,因此我们预处理出cnt[i][j]表示小集合i与大集合j的交集元素个数,在小集合更新时,枚举所有大集合,该 sum[j]+=val * cnt[i][j] ;在输出小集合和时,同理枚举所有大集合,加上Lazy[j] * cnt[i][j]

  • 代码

#include<bits/stdc++.h>
using namespace std;
const int N=1e5+5;
const int M=400;
typedef long long ll;
ll a[N],sum[N],Add[N];
vector<int> V[N],tot1[N],tot2[N],B;
int cnt[5005][5005],cnt2[M][M],num[N],cb,cm;
bool mark[N];
int main() {
	int n,m,q,sq;
	scanf("%d%d%d",&n,&m,&q);
	sq=(int)(sqrt(n));
	for(int i=1;i<=n;i++) {
		scanf("%lld",&a[i]);
	}
	for(int i=1;i<=m;i++) {
		scanf("%d",&num[i]);
		if(num[i]>sq) B.push_back(i);
		ll res=0;
		for(int j=1;j<=num[i];j++) {
			int x; scanf("%d",&x);
			V[i].push_back(x); res+=a[x];
		}
		if(num[i]>sq) sum[i]=res;
	}
	int j;
	for(int i=1;i<=m;i++) {
		if(num[i]<=sq) {
			for(j=0;j<V[i].size();j++) tot1[V[i][j]].push_back(1ll*i);
		}
		else {
			for(j=0;j<V[i].size();j++) tot2[V[i][j]].push_back(1ll*i);
		}
	}
	for(int k=1;k<=n;k++) {
		for(int i=0;i<tot2[k].size();i++) {
			for(j=0;j<tot2[k].size();j++) if(i!=j)cnt2[tot2[k][i]][tot2[k][j]]++;
			for(j=0;j<tot1[k].size();j++) {
				cnt[tot2[k][i]][tot1[k][j]]++;
			}
		}
	}
	for(int t=1;t<=q;t++) {
		char ch[5];
		int x,y;
		scanf("%s",ch);
		if(ch[0]=='?') {
			scanf("%d",&x);
			if(num[x]<=sq) {
				ll res=0;
				for(j=0;j<V[x].size();j++)res+=a[V[x][j]];
				for(j=0;j<B.size();j++) {
					res+=Add[B[j]]*cnt[B[j]][x];
				}
				printf("%lld\n",res);
			}
			else {
				ll res=0;
				for(j=0;j<B.size();j++) {
//					printf("%d %d %d\n",B[j],cnt2[x][B[j]],Add[B[j]]);
					res+=Add[B[j]]*cnt2[x][B[j]];
				}
				printf("%lld\n",sum[x]+Add[x]*num[x]+res);
			}
		}
		else {
			scanf("%d%d",&x,&y);
			if(num[x]<=sq) {
				for(j=0;j<V[x].size();j++)a[V[x][j]]+=y;
				for(j=0;j<B.size();j++) sum[B[j]]+=y*cnt[B[j]][x];
			}
			else {
				Add[x]+=y;
			}
		}
	}
	return 0;
}

雅加达的摩天楼

  • 思路:
    啊这读完题都会感受到最短路那味,可是恶心人的是,怎么也想不到要用根号分治优化建图.还有这道题跑最短路是以摩天楼为点(不是doge)

  • \(p_i>\sqrt{n}\)时,直接暴力将\(b_i\)向其它能到的点直接连边

  • 否则,建一个图框架:令 \(len=\sqrt{n}\),将每个点拆分成len个虚拟节点+1个它本身,显然len个虚拟节点都要指向它本身。有len个虚拟层,如第k层的节点分别为(0~n-1的第k个的虚拟节点构成)
    第k个虚拟层,将里面的点每隔k距离连边。
    最后,我们将\(b_i\)\(b_i\)的第\(p_i\)个虚拟节点连边
    ps.代码中节点编号还是按偏移量n来表现
    总结一下,这利用了广度撒网的思想来顶住复杂度上限(注意常数)

  • 代码:

#include<bits/stdc++.h>
using namespace std;
const int N=1e5+5;
const int M=2e7;
const int NM=1e7+5;
int n,m;
int len,B[N],p[N];
int nxt[M],to[M],head[NM],ln[M],dis[NM],num;
bool mark[NM];
struct node {
	int p,w;
	bool operator<(const node &u) const{return w>u.w;}
};
void add_edge(int u,int v,int w) {
	nxt[++num]=head[u],to[num]=v,ln[num]=w,head[u]=num;
}

void Build1(int x) {
	for(int i=B[x]+p[x],j=1;i<n;i+=p[x],j++) {
		add_edge(B[x],i,j);
	}
	for(int i=B[x]-p[x],j=1;i>=0;i-=p[x],j++) {
		add_edge(B[x],i,j);
	}
}
void init() {
	//建立len个维度
	for(int s=1;s<=len;s++) {
		for(int i=0;i<n-s;i++) {
			add_edge(i+s*n,i+s+s*n,1),add_edge(i+s+s*n,i+s*n,1);
		}
	}
	for(int i=0;i<n;i++) {
		for(int s=1;s<=len;s++) {
			add_edge(i+s*n,i,0);
		}
	}
}
priority_queue<node> Q;
void DJ(int s) {
	memset(dis,0x3f,sizeof(dis));
	dis[s]=0;
	Q.push((node){s,0});
	while(!Q.empty()) {
		int u=Q.top().p; Q.pop();
		if(mark[u]) continue;
		mark[u]=true;
		for(int i=head[u];i;i=nxt[i]) {
			int v=to[i];
			if(!mark[v]&&dis[v]>dis[u]+ln[i]) {
				dis[v]=dis[u]+ln[i];
				Q.push((node){v,dis[v]});
			}
		} 
	}
}
int main() {
	scanf("%d%d",&n,&m);
	len=sqrt(n);
	len=min(len,100);
	init();
	for(int i=0;i<m;i++) scanf("%d%d",&B[i],&p[i]);
	for(int i=0;i<m;i++) {
		if(p[i]>len) Build1(i);
		else add_edge(B[i],B[i]+p[i]*n,0);
	}
	DJ(B[0]);
	if(dis[B[1]]==0x3f3f3f3f) printf("-1");
	else printf("%d\n",dis[B[1]]);
	return 0;
} 

跑步

  • 思路:
    类上
  • 代码:
#include<bits/stdc++.h>
using namespace std;
const int N=1e5+5;
typedef long long ll;
ll f[N][2],g[N][2],sum[N];
int main() {
	int n,k;ll p;
	scanf("%d%lld",&n,&p);
	k=(int)(sqrt(n));
	f[0][0]=f[0][1]=1;
	g[0][0]=1;
	for(int j=1;j<k;j++) {
		for(int i=1;i<=n;i++) {
			f[i][j&1]=f[i][j&1^1];
			if(i>=j) f[i][j&1]+=f[i-j][j&1],f[i][j&1]%=p;
		}
	}
	sum[0]=1;
	for(int j=1;j<=n/k;j++) {
		for(int i=0;i<=n;i++) g[i][j&1]=0;
		for(int i=k;i<=n;i++) {
			g[i][j&1]=g[i-k][j&1^1];
			if(i>=j) g[i][j&1]+=g[i-j][j&1],g[i][j&1]%=p;
			sum[i]+=g[i][j&1],sum[i]%=p;
		}
	}
	ll ans=0;
	for(int i=0;i<=n;i++) {
		ans+=f[i][(k-1)&1]*sum[n-i]%p,ans%=p;
	}
	printf("%lld",ans);
	return 0; 
}