「算法笔记」从【伊卡洛斯】谈数据分治

引子

有时候,一些类似毒瘤数据结构的题目乍一看似乎十分令人头疼。它们往往是要求你统计某种元素的个数。实际上,这些题目有些不是数据结构题,而是使用了数据分治的思想对基本的程序进行优化。


例题——伊卡洛斯

题目大意:给定一个数列,每次询问一个区间内数的乘积的因数个数 mod1000000007 m o d 1000000007 的结果。 n,m100000 n , m ≤ 100000 ai1000000 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=1pin ∏ 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 的数仅有一个>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 的素数有几个。
期望得分100分。

代码

#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 的大小做比较,从而对程序进行进一步优化。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值