引子
有时候,一些类似毒瘤数据结构的题目乍一看似乎十分令人头疼。它们往往是要求你统计某种元素的个数。实际上,这些题目有些不是数据结构题,而是使用了数据分治的思想对基本的程序进行优化。
例题——伊卡洛斯
题目大意:给定一个数列,每次询问一个区间内数的乘积的因数个数 mod1000000007 m o d 1000000007 的结果。 n,m≤100000 n , m ≤ 100000 , ai≤1000000 a i ≤ 1000000
题解
性质: τ(x)=∏ki=1αi+1 τ ( x ) = ∏ i = 1 k α i + 1 ( x=∏ki=1pαii x = ∏ i = 1 k p i α i )
朴素算法
首先一看到这种题目就想到莫队。(这是直觉)
我们记下区间中每一个素因子的出现次数即可。
详见代码。
时间复杂度
O(n×n‾√×func(maxai))
O
(
n
×
n
×
f
u
n
c
(
max
a
i
)
)
。
其中
func
f
u
n
c
函数的意义是
∏func(n)−1i=1pi<n
∏
i
=
1
f
u
n
c
(
n
)
−
1
p
i
<
n
且
∏func(n)i=1pi≥n
∏
i
=
1
f
u
n
c
(
n
)
p
i
≥
n
。
(
func(106)=6
f
u
n
c
(
10
6
)
=
6
)
期望得分
70
70
分。
代码
#include <bits/stdc++.h>
#define K 1000000007
#define M 1000000
#define N 100000
#define L 20
#define pb push_back
using namespace std;
typedef long long ll;
vector <int> pri,fen[N|1],cnt[N|1];
int n,m,lft,rht,ans,out[N|1],reg[M|1],num[N*L|1];
bool isp[M|1];
struct query {
int ord,lbnd,rbnd,lump;
bool operator < (const query &o) const {
if(lump==o.lump)
return rbnd<o.rbnd;
return lump<o.lump;
}
} q[N|1];
int fastpow(int x,int y) {
int res=1,cur=x;
for(int i=0;(1<<i)<=y;i++) {
if((1<<i)&y)
res=1ll*res*cur%K;
cur=1ll*cur*cur%K;
}
return res;
}
void initpri() {
memset(isp,1,sizeof(isp)),isp[0]=isp[1]=0;
for(int i=2;i<=M;i++) {
if(isp[i]) pri.pb(i);
for(int j=0;j<pri.size()&&i*pri[j]<=M;j++) {
isp[i*pri[j]]=0;
if(i%pri[j]==0) break;
}
}
for(int i=1;i<=N*L;i++)
num[i]=fastpow(i,K-2);
}
void readnum() {
scanf("%d%d",&n,&m);
for(int t,i=1;i<=n;i++) {
scanf("%d",&t);
for(int j=0,k=0;j<pri.size()&&pri[j]*pri[j]<=t;j++,k=0) if(t%pri[j]==0) {
for(;t%pri[j]==0;t/=pri[j],k++);
fen[i].pb(pri[j]),cnt[i].pb(k);
} if(t>1) fen[i].pb(t),cnt[i].pb(1);
}
}
void mosolve() {
int l=sqrt(n);
for(int i=1;i<=m;i++) {
scanf("%d%d",&q[i].lbnd,&q[i].rbnd);
q[i].ord=i,q[i].lump=q[i].lbnd/l;
}
sort(q+1,q+1+m);
for(int i=1;i<=M;i++)
reg[i]=1;
lft=rht=ans=1;
for(int i=0;i<cnt[1].size();i++) {
reg[fen[1][i]]+=cnt[1][i];
ans=1ll*ans*(reg[fen[1][i]])%K;
}
for(int i=1;i<=m;i++) {
for(;lft>q[i].lbnd;lft--) {
for(int j=0;j<cnt[lft-1].size();j++) {
ans=1ll*ans*num[reg[fen[lft-1][j]]]%K;
reg[fen[lft-1][j]]+=cnt[lft-1][j];
ans=1ll*ans*reg[fen[lft-1][j]]%K;
}
}
for(;rht<q[i].rbnd;rht++) {
for(int j=0;j<cnt[rht+1].size();j++) {
ans=1ll*ans*num[reg[fen[rht+1][j]]]%K;
reg[fen[rht+1][j]]+=cnt[rht+1][j];
ans=1ll*ans*reg[fen[rht+1][j]]%K;
}
}
for(;lft<q[i].lbnd;lft++) {
for(int j=0;j<cnt[lft].size();j++) {
ans=1ll*ans*num[reg[fen[lft][j]]]%K;
reg[fen[lft][j]]-=cnt[lft][j];
ans=1ll*ans*reg[fen[lft][j]]%K;
}
}
for(;rht>q[i].rbnd;rht--) {
for(int j=0;j<cnt[rht].size();j++) {
ans=1ll*ans*num[reg[fen[rht][j]]]%K;
reg[fen[rht][j]]-=cnt[rht][j];
ans=1ll*ans*reg[fen[rht][j]]%K;
}
}
out[q[i].ord]=ans;
}
for(int i=1;i<=m;i++)
printf("%d\n",out[i]);
}
int main() {
initpri();
readnum();
mosolve();
return 0;
}
优化
我们对这些素因数分情况讨论。
当它们小于等于
n‾√
n
时,我们使用前缀和记录它们的出现个数。
当它们大于
n‾√
n
时,我们使用刚才的方法计算他们的出现个数。
同时我们发现,小于等于
n
n
的数仅有一个的素因子。
所以,我们每次移动区间时只需将
cnt
c
n
t
数组中的一个数改变。
时间复杂度
O(n×(n‾√+primecount(n‾√)))
O
(
n
×
(
n
+
p
r
i
m
e
c
o
u
n
t
(
n
)
)
)
。
其中
primecount(x)
p
r
i
m
e
c
o
u
n
t
(
x
)
表示小于等于
x
x
的素数有几个。
期望得分分。
代码
#include <cmath>
#include <cstdio>
#include <algorithm>
#define mxn 100000
#define mxm 1000000
#define mxk 168
#define hfm 1000
const int mod=1e9+7;
using namespace std;
bool inp[mxn|1];
int n,m,k,l,r,ans,lump,a[mxn|1],pri[mxm|1],inv[mxn|1];
int sum[mxn|1][mxk|1],cnt[mxm|1],res[mxn|1];
struct query {
int id,l,r;
bool operator<(const query&o) const {
return l/lump==o.l/lump?r<o.r:l/lump<o.l/lump;
}
} q[mxn|1];
void prework() {
lump=sqrt(n+0.5);
for(int i=2;i<=hfm;i++)
if(!inp[i]) {
pri[++k]=i;
for(int j=2*i;j<=hfm;j+=i)
inp[j]=1;
}
inv[0]=inv[1]=1;
for(int i=2;i<=n;i++)
inv[i]=1ll*(mod-mod/i)*inv[mod%i]%mod;
}
void add(int i,int x) {
if(a[i]!=1) {
ans=1ll*ans*inv[cnt[a[i]]+1]%mod;
cnt[a[i]]+=x;
ans=1ll*ans*(cnt[a[i]]+1)%mod;
}
}
int main() {
scanf("%d%d",&n,&m);
prework();
for(int i=1;i<=n;i++) {
scanf("%d",&a[i]);
for(int j=1;j<=mxk;j++) {
sum[i][j]=sum[i-1][j];
while(a[i]%pri[j]==0) {
a[i]/=pri[j];
sum[i][j]++;
}
}
}
for(int i=1;i<=m;i++)
q[i].id=i,scanf("%d%d",&q[i].l,&q[i].r);
sort(q+1,q+m+1);
l=2,r=1,ans=1;
for(int i=1;i<=n;i++) {
while(l>q[i].l) add(--l,1);
while(r<q[i].r) add(++r,1);
while(l<q[i].l) add(l++,-1);
while(r>q[i].r) add(r--,-1);
int tmp=1;
for(int i=1;i<=mxk;i++)
tmp=1ll*tmp*(sum[r][i]-sum[l-1][i]+1)%mod;
res[q[i].id]=1ll*tmp*ans%mod;
}
for(int i=1;i<=n;i++)
printf("%d\n",res[i]);
return 0;
}
小试牛刀
题目链接:Graph
题目大意:给定一个带权无向图,每个节点有黑白两种颜色。有两种操作:
1.询问两端颜色分别为u和v的边的权值和
2.修改某点的颜色
题解
代码
#include <cmath>
#include <cstdio>
#include <cstring>
#include <map>
#define mxn 100000
#define pii pair<int,int>
#define ppi pair<pii,int>
#define mkp make_pair
typedef long long ll;
using namespace std;
char op[9]; int q,x,y;
map<pii,ll> mp;
ll ans[3],sum[mxn|1][2],u[mxn|1],v[mxn|1],w[mxn|1],wei[mxn<<1|1];
int t,n,m,col[mxn|1],typ[mxn|1],d[mxn|1];
int siz,tot,lnk[mxn|1][2],ter[mxn<<1|1],nxt[mxn<<1|1];
void add(int u,int v,ll w,int b) {
ter[++tot]=v; wei[tot]=w;
nxt[tot]=lnk[u][b]; lnk[u][b]=tot;
}
int main() {
for(t=1;~scanf("%d%d",&n,&m);t++) {
for(int i=1;i<=n;i++) scanf("%d",col+i); mp.clear();
for(int u,v,w,i=1;i<=m;i++) {
scanf("%d%d%d",&u,&v,&w);
if(u>v) swap(u,v);
mp[mkp(u,v)]+=w;
} m=0;
for(map<pii,ll>::iterator it=mp.begin();it!=mp.end();it++)
u[++m]=(*it).first.first,v[m]=(*it).first.second,w[m]=(*it).second;
memset(d,0,sizeof(d)); siz=sqrt(n+0.5);
for(int i=1;i<=m;i++) d[u[i]]++,d[v[i]]++;
for(int i=1;i<=n;i++) typ[i]=(d[i]>=siz);
memset(lnk,0,sizeof(lnk)); tot=0;
for(int i=1;i<=m;i++) {
if(typ[u[i]]) add(v[i],u[i],w[i],1);
else add(u[i],v[i],w[i],0);
if(typ[v[i]]) add(u[i],v[i],w[i],1);
else add(v[i],u[i],w[i],0);
}
memset(sum,0,sizeof(sum)); ans[0]=ans[1]=ans[2]=0;
for(int i=1;i<=m;i++) {
if(typ[u[i]]) sum[u[i]][col[v[i]]]+=w[i];
if(typ[v[i]]) sum[v[i]][col[u[i]]]+=w[i];
ans[col[u[i]]+col[v[i]]]+=w[i];
}
for(printf("Case %d:\n",t),scanf("%d",&q);q;q--) {
scanf("%s%d",op,&x);
if(op[0]=='A') {
scanf("%d",&y);
printf("%lld\n",ans[x+y]);
continue;
}
col[x]^=1;
if(typ[x]) {
for(int i=0;i<=1;i++) {
ans[(col[x]^1)+i]-=sum[x][i];
ans[col[x]+i]+=sum[x][i];
}
} else {
for(int i=lnk[x][0];i;i=nxt[i]) {
ans[(col[x]^1)+col[ter[i]]]-=wei[i];
ans[col[x]+col[ter[i]]]+=wei[i];
}
}
for(int i=lnk[x][1];i;i=nxt[i]) {
sum[ter[i]][col[x]^1]-=wei[i];
sum[ter[i]][col[x]]+=wei[i];
}
}
}
return 0;
}
总结
数据分治思想就是对于某个值进行分类讨论,通常是与 n‾√ n 的大小做比较,从而对程序进行进一步优化。