1. 程式人生 > >「演算法筆記」從【伊卡洛斯】談資料分治

「演算法筆記」從【伊卡洛斯】談資料分治

引子

有時候,一些類似毒瘤資料結構的題目乍一看似乎十分令人頭疼。它們往往是要求你統計某種元素的個數。實際上,這些題目有些不是資料結構題,而是使用了資料分治的思想對基本的程式進行優化。

例題——伊卡洛斯

題目大意:給定一個數列,每次詢問一個區間內數的乘積的因數個數mod1000000007的結果。n,m100000ai1000000

題解

性質:τ(x)=i=1kαi+1x=i=1kpiαi

樸素演算法
首先一看到這種題目就想到莫隊。(這是直覺)
我們記下區間中每一個素因子的出現次數即可。
詳見程式碼。
時間複雜度

O(n×n×func(maxai))
其中func函式的意義是i=1func(n)1pi<ni=1func(n)pin
func(106)=6
期望得分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的素因子。
所以,我們每次移動區間時只需將cnt陣列中的一個數改變。
時間複雜度O(n×(n+primecount(n)))
其中primecount(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=