1. 程式人生 > 實用技巧 >題解 P4887 【【模板】莫隊二次離線(第十四分塊(前體))】

題解 P4887 【【模板】莫隊二次離線(第十四分塊(前體))】

原文連結

莫隊二次離線

最近學了這個黑科技,來寫篇題解分享一下。

順便\(orz\;lxl\)

Part.1 問題引入

有一個序列,每次查詢一個區間中有幾個二元組的的異或值在二進位制下有\(k\)\(1\)

考慮莫隊。

先用莫隊轉化問題,先考慮我們已經求出\([l,r]\)的答案,要將\(r+1\)加入答案,其他的轉移類似,現在問題轉化為求\(r+1\)\([l,r]\)中有幾個數的異或值有\(k\)\(1\)

Part.2 暴力

考慮暴力怎麼做,由於值域很小\((a_i\leq 16384)\),我們可以先預處理出一個數組\(x\)中儲存有哪些數在二進位制數下有\(k\)個1,每一次插入一個數\(a_i\)

,就把它與\(x\)陣列中所有的數異或起來的值放進一個桶裡,然後查詢就直接查當前數在桶中出現了幾次。為什麼呢?

比如我們有\(a_i\;xor\;a_j=x\),就有\(a_i\;xor\;x=a_j\),我們已經將\(a_i\;xor\;x\)放入桶裡,直接查就可以了。

但這樣有個問題,莫隊有\(n\sqrt n\)次轉移,每次修改時如果真的暴力插入桶的話,複雜度就是\(O(n\sqrt n\times x)\)(設\(x\)為陣列中的元素個數),顯然是不對的,考慮進一步優化。

Part.3 解決

我們進一步挖掘性質,發現我們要求的是\(r+1\)\([l,r]\)的貢獻,實際上我們可以先求它對\([1,r]\)

的貢獻,再減去\([1,l-1]\)的貢獻,為什麼要這麼做呢?我們可以發現\(r+1\)對前者的貢獻是可以\(O(n\times x)\)是可以預處理的,就只用考慮後者的貢獻了。

如下圖,我們考慮我們已經算出了紅色部分的貢獻了。

然後我們需要轉移綠色部分的貢獻。

這個過程中我們可以發現,左端點是不變,即我們的\([1,l-1]\)的答案是不變的,即不用修改,也就是說,我們只有查詢操作,而查詢操作時\(O(1)\)的!

所以我們二次離線,先跑一次莫隊,將如上的綠色區間儲存下來,再統一處理。

具體實現可以每個點開一個\(vector\),然後移動右端點的時,我們將如上的綠色區間儲存在左端點,移動左端點時,將綠色區間儲存在右端點。

然後我們把這些\(vector\)掃一遍,修改就只有在從\(i\)\(i+1\)時才有,查詢還是\(O(n\sqrt n)\)次,但單次是\(O(1)\)的。

所以我們的總複雜的就變成了\(O(n\sqrt n+n\times x)\)

總結一下,對於那種查詢複雜度很高的東西,並且同時詢問資訊還可以差分的問題,我們就可以用莫隊二次離線解決。

做完這題可以去做一下這個,稍微要複雜一點,需要一些其他的資料結構輔助。

這個演算法理解其實並不難,主要是程式碼細節比較多,建議儘量先自己實現,遇到問題再看題解的程式碼。

struct Query_Node
{
    int l,r,id,kl;
    ll ans;
    bool operator < (const Query_Node &u) const{
        return kl!=u.kl?kl<u.kl:(kl&1?r<u.r:r>u.r);
    }
}q[maxn];

struct Node
{
    int l,r,id;
    Node(int l=0,int r=0,int id=0) : l(l),r(r),id(id) {}
};
ll ans[maxn];
vector<Node> two[maxn];
int n,m,k,a[maxn],tax[maxn],klen,pre[maxn];
int isu[maxn],ist;

inline int calc(int x)
{
    int ans=0;
    while(x) ++ans,x-=(x&(-x));
    return ans;
}

template<typename T>
inline void read(T &x)
{
    char c;int f=1;
    while(!isdigit(c=getchar())) (c=='-')&&(f=-1);
    x=c^48;
    while(isdigit(c=getchar())) x=x*10+(c^48);
    x*=f;
}

int main()
{
    read(n);read(m);read(k);
    for(int i=1;i<=n;++i)
        read(a[i]);
    klen=n/sqrt(m*2/3);
    for(int i=1;i<=m;++i)
    {
        read(q[i].l);read(q[i].r);q[i].id=i;
        q[i].kl=(q[i].l-1)/klen+1;
    }
    sort(q+1,q+m+1);
    for(int i=0;i<=maxa;++i) if(calc(i)==k) isu[++ist]=i;
    for(int i=1;i<=n;++i)
    {
        for(int j=1;j<=ist;++j) ++tax[isu[j]^a[i]];
        pre[i]=tax[a[i+1]];
    }
    for(int i=1,l=1,r=0;i<=m;++i)//先空跑莫隊,找出綠色區間,同時處理前者的貢獻
    {
        if(l<q[i].l) two[r].push_back(Node(l,q[i].l-1,-i));
        while(l<q[i].l) q[i].ans+=pre[l-1],++l;
        if(l>q[i].l) two[r].push_back(Node(q[i].l,l-1,i));
        while(l>q[i].l) q[i].ans-=pre[l-2],--l;
        if(r<q[i].r) two[l-1].push_back(Node(r+1,q[i].r,-i));
        while(r<q[i].r) q[i].ans+=pre[r],++r;
        if(r>q[i].r) two[l-1].push_back(Node(q[i].r+1,r,i));
        while(r>q[i].r) q[i].ans-=pre[r-1],--r;
    }
    memset(tax,0,sizeof(tax));
    //二次離線,處理後者的貢獻
    for(int i=1;i<=n;++i)
    {
        for(int j=1;j<=ist;++j) ++tax[isu[j]^a[i]];
        for(vector<Node>::iterator it=two[i].begin();it!=two[i].end();++it)
            for(int j=it->l;j<=it->r;++j)
            {
                int tmp=tax[a[j]];
                if(j<=i&&k==0) tmp--;
                if(it->id<0) q[-it->id].ans-=tmp;
                else q[it->id].ans+=tmp;
            }
    }
    for(int i=1;i<=m;++i) q[i].ans+=q[i-1].ans,ans[q[i].id]=q[i].ans;
    for(int i=1;i<=m;++i)
        printf("%lld\n",ans[i]);
    return 0;
}