1. 程式人生 > >BZOJ4556:[Tjoi2016&Heoi2016]字串 (字尾自動機+樹上倍增+二分答案+線段樹合併)

BZOJ4556:[Tjoi2016&Heoi2016]字串 (字尾自動機+樹上倍增+二分答案+線段樹合併)

題目分析:我發現我對線段樹合併一無所知QAQ。

先講一種簡單的做法:我們可以將字尾陣列建出來,對於每個詢問二分一個答案mid。然後從Rank[c]往上下兩個方向跳,找到一個區間[L,R],使得這個區間的字尾和c開頭的字尾的LCP大於等於mid。那麼如果sa[L]~sa[R]中有落在[a,b-mid+1]的數,最終答案就大於等於mid。要判斷區間[L,R]中是否有屬於某個權值區域的數,寫個可持久化的權值線段樹,然後差分一下就行。

上面的做法程式碼很短,但常數偏大。其實還可以把反串的字尾自動機建出來,二分完答案mid後,用樹上倍增跳到Right(c)-mid所對應的節點,然後看這個節點的Right集中是否包含[b+mid-1,a]中的數(上文的a,b,c均指在反串中的位置)。這可以用線段樹合併來實現(當然也可以用DFS序+可持久化線段樹,不過我做這題的主要目的是寫一寫線段樹合併)。

為了在省選的賽場上也能碼出這種超級資料結構題,我特地測試了一下自己寫這題程式碼的時間。結果我用75min寫好了程式碼,交上去WA了一次,然後用一組手造的小資料檢查了一遍,發現有個n+1寫成了n,還有線段樹合併的時候忘了新建節點qaq,然後再交了一次就……

就不停地RE!!!我不停地debug還是沒有發現任何錯。到了第二天中午(也就是今天中午),我迫不得已把網上某位dalao的程式copy下來對拍,還是沒有拍出任何錯QAQ。於是我只好將程式碼一段一段註釋掉,交上BZOJ,看看是哪裡RE了。結果我發現是線段樹合併出錯了。

假設線段樹的葉子節點有N個,那麼線段樹合併的總空間應該是2Nlog(

N)(至少我寫的是這樣),而我只開了Nlog(N)。這個應該比較好證明:考慮權值線段樹上某個代表區間[L,R]的節點,一開始每一個數都要單獨開一條鏈,於是這個位置的節點被新建了R-L+1次。然後區間裡的每兩個數合併,都要新開一次該位置的節點,所以又要最多開R-L次,於是所有節點加起來就是2Nlog(N)的空間。

那為什麼對拍不出錯呢?因為我的字串是隨機生成的,導致建出來的SAM狀態數很少。而線段樹的葉子節點個數等於SAM的狀態數,所以不會爆。我將空間改成2Nlog(N)之後MLE了,於是我稍微調小了點空間,終於過了。然後我就對著這道題花掉了75min寫程式碼+一箇中午debug的時間……

CODE:

#include<iostream>
#include<string>
#include<cstring>
#include<cstdio>
#include<cstdlib>
#include<stdio.h>
#include<algorithm>
using namespace std;

const int maxn=100100;
const int maxm=7000000;
const int maxl=20;
const int maxc=26;

struct Seg
{
    int sum;
    Seg *lson,*rson;
} tree[maxm];
int Tcur=-1;

struct Tnode;

struct edge
{
    Tnode *obj;
    edge *Next;
} e[maxn<<1];
int Ecur=-1;

struct Tnode
{
    int val,Right;
    edge *head;
    Seg *Seg_Root;
    Tnode *son[maxc],*Fa[maxl],*parent;
} SAM[maxn<<1];
Tnode *Last[maxn];
Tnode *Root;
int cur=-1;

int a[maxn];
char s[maxn];
int n,m;
int A,b,c,d;

Seg *New_Seg()
{
    Tcur++;
    tree[Tcur].sum=0;
    tree[Tcur].lson=tree[Tcur].rson=tree;
    return tree+Tcur;
}

Tnode *New_node(int v,int c)
{
    cur++;
    SAM[cur].val=v;
    SAM[cur].Right=c;
    SAM[cur].parent=NULL;
    SAM[cur].head=NULL;
    SAM[cur].Seg_Root=tree;
    for (int i=0; i<maxc; i++) SAM[cur].son[i]=NULL;
    for (int i=0; i<maxl; i++) SAM[cur].Fa[i]=NULL;
    return SAM+cur;
}

void Add(Tnode *x,Tnode *y)
{
    Ecur++;
    e[Ecur].obj=y;
    e[Ecur].Next=x->head;
    x->head=e+Ecur;
}

void Update(Seg *&root,int L,int R,int x)
{
    if (root==tree) root=New_Seg();
    if (L==R) root->sum=1;
    else
    {
        int mid=(L+R)>>1;
        if (x<=mid) Update(root->lson,L,mid,x);
        else Update(root->rson,mid+1,R,x);
        root->sum=root->lson->sum+root->rson->sum;
    }
}

void Merge(Seg *&x,Seg *y)
{
    if (y==tree) return;
    if (x==tree)
    {
        x=y;
        return;
    }

    Seg *z=x;
    x=New_Seg();
    (*x)=(*z); //要記得新開節點!!!
    Merge(x->lson,y->lson);
    Merge(x->rson,y->rson);
    x->sum=x->lson->sum+x->rson->sum;
}

void Dfs(Tnode *node)
{
    Update(node->Seg_Root,1,n+1,node->Right);
    for (edge *p=node->head; p; p=p->Next)
    {
        Tnode *to=p->obj;
        Dfs(to);
        Merge(node->Seg_Root,to->Seg_Root);
//我這裡的線段樹合併要兩倍SAM節點數*log(n)的空間,也就是maxn*maxl<<2(注意被卡空間)!!!
    }
}

void Build_SAM()
{
    Root=New_node(0,n+1);
    Last[0]=Root;
    for (int i=1; i<=n; i++)
    {
        Tnode *P=Last[i-1],*NP=New_node(i,i);
        Last[i]=NP;
        int to=a[i];
        while ( P && !P->son[to] ) P->son[to]=NP,P=P->parent;
        if (!P) NP->parent=Root;
        else
        {
            Tnode *Q=P->son[to];
            if (P->val+1==Q->val) NP->parent=Q;
            else
            {
                Tnode *NQ=New_node(P->val+1,n+1);
                for (int i=0; i<maxc; i++) NQ->son[i]=Q->son[i];
                NQ->parent=Q->parent;
                NP->parent=Q->parent=NQ;
                while ( P && P->son[to]==Q ) P->son[to]=NQ,P=P->parent;
            }
        }
    }

    for (int i=0; i<=cur; i++)
    {
        Tnode *P=SAM+i;
        P->Fa[0]=P->parent;
    }
    for (int j=1; j<maxl; j++)
        for (int i=0; i<=cur; i++)
        {
            Tnode *P=SAM+i;
            if (P->Fa[j-1]) P->Fa[j]=P->Fa[j-1]->Fa[j-1];
        }

    for (int i=1; i<=cur; i++)
    {
        Tnode *P=SAM+i;
        Add(P->parent,P);
    }
    Dfs(Root);
}

Tnode *Jump(int x,int y)
{
    Tnode *P=Last[x];
    for (int j=maxl-1; j>=0; j--)
        if ( P->Fa[j] && P->Fa[j]->val>=y ) P=P->Fa[j];
    return P;
}

bool Query(Seg *root,int L,int R,int x,int y)
{
    if ( y<L || R<x || root==tree ) return false;
    if ( x<=L && R<=y ) return root->sum;

    int mid=(L+R)>>1;
    bool fL=Query(root->lson,L,mid,x,y);
    bool fR=Query(root->rson,mid+1,R,x,y);
    return ( fL || fR );
}

bool Judge(int len)
{
    Tnode *node=Jump(c,len);
    return Query(node->Seg_Root,1,n+1,b+len-1,A);
}

int Binary()
{
    int L=0,R=min(A-b+1,c-d+1)+1;
    while (L+1<R)
    {
        int mid=(L+R)>>1;
        if ( Judge(mid) ) L=mid;
        else R=mid;
    }
    return L;
}

int main()
{
    freopen("4556.in","r",stdin);
    freopen("4556.out","w",stdout);

    scanf("%d%d",&n,&m);
    scanf("%s",&s);
    for (int i=1; i<=n; i++) a[i]=s[i-1]-'a';
    for (int i=1; i<=(n>>1); i++) swap(a[i],a[n-i+1]);

    New_Seg();
    Build_SAM();

    for (int i=1; i<=m; i++)
    {
        scanf("%d%d%d%d",&A,&b,&c,&d);
        A=n-A+1;
        b=n-b+1;
        c=n-c+1;
        d=n-d+1;
        int ans=Binary();
        printf("%d\n",ans);
    }

    return 0;
}

相關推薦

BZOJ4556[Tjoi2016&Heoi2016]字串 字尾自動機+樹上倍增+二分答案+線段合併

題目分析:我發現我對線段樹合併一無所知QAQ。 先講一種簡單的做法:我們可以將字尾陣列建出來,對於每個詢問二分一個答案mid。然後從Rank[c]往上下兩個方向跳,找到一個區間[L,R],使得這個區間的字尾和c開頭的字尾的LCP大於等於mid。那麼如果

bzoj 3473: 字串 字尾自動機

題目描述 傳送門 題目大意:給定n個字串,詢問每個字串有多少子串(不包括空串)是所有n個字串中至少k個字串的子串? 題解 同bzoj3277 程式碼 #include<iost

BZOJ 4552 [Tjoi2016&Heoi2016]排序二分答案 + 線段給自己始終如一的提醒

在說二分之前,我覺得這道題ai的值域是<= n ,這不是就聯想到權值線段樹/樹狀陣列了嗎?(事實上也確實有這樣的做法) 不過因為這是我聽二分課的例題就果斷二分了 如果二分答案的話,我們定義b[i] , 且a[i] > mid則b[i] = 1 ,否

P5161 WD與數列字尾自動機+線段合併

傳送門 沒想出來→_→ 首先不難看出要差分之後計算不相交也不相鄰的相等子串對數,於是差分之後建SAM,在parent樹上用線段樹合併維護endpos集合,然後用啟發式合併維護一個節點對另一個節點的貢獻,於是總的時間複雜度為\(O(n\log^2n)\) //minamoto #include<bit

bzoj 4552 [Tjoi2016&Heoi2016]排序 (二分答案 線段

print %d 線段樹 https 代碼 數組 pda lse 線段 題目鏈接:https://www.lydsy.com/JudgeOnline/problem.php?id=4552 題意: 給你一個1-n的全排列,m次操作,操作由兩種:1.將[l,r]升序排序,

簡單的字串迴文子集數+二維線段

題目: 思路:第一步先處理如何計算一個區間的迴文子集個數 構成迴文串,有兩種情況: 1. 每種字母都選偶數個,這樣一定可以構成迴文串                     &nbs

線段CDOJ1591-An easy problem A RMQ演算法和最簡單的線段模板

An easy problem A Time Limit: 1000/1000MS (Java/Others) Memory Limit: 65535/65535KB (Java/Others) Problem Description N個數排

BZOJ3277 串後綴數組+二分答案+主席

void log 統計 lld ostream i++ con amp code   因為不會SAM,考慮SA。將所有串連起來並加分隔符,每次考慮計算以某個位置開始的子串有多少個合法。   對此首先二分答案,找到名次數組上的一個區間,那麽只需要統計有多少個所給串在該區間內出

Gym - 101194G Pandaria 並查集+倍增+線段合併

  題意: 給定一個無向圖。每個點有一種顏色。現在給定q個詢問,每次詢問x和w,求所有能通過邊權值不超過w的邊走到x的點的集合中,哪一種顏色的點出現的次數最多。次數相同時輸出編號最小的那個顏色。強制線上。   解題思路:膜拜大神們的程式碼!看了好久,終於

[BZOJ2212][Poi2011]Tree Rotations線段合併

Address 洛谷 P3521 BZOJ 2212 LOJ #2163 Solution 非常有意思的題 一個直觀的想法 對於一個點 u

[BZOJ3307]雨天的尾巴LCA + 線段合併

Address 洛谷 P4556 BZOJ 3307 Solution 首先轉化一下問題,考慮一種顏色 c

【bzoj2333 & luoguP3273】棘手的操作線段合併

  題目傳送門:bzoj2333 luoguP3273   這操作還真“棘手”。。聽說這題是可並堆題?然而我不會可並堆。於是我就寫了線段數合併,然後調了一晚上,資料結構毀一生!!!QAQ……   其實這題也可以把合併強行看成樹上的關係然後dfs序後直接線段樹的,然而我菜啊。。看到連邊就只能想到線

loj#2553. 「CTSC2018」暴力寫掛邊分治+線段合併

傳送門 題解: 按照套路,變成求d1(x,y)+dx+dy−2∗d′lca′(x,y)d_1(x,y)+d_x+d_y-2*d&#x27;lca&#x27;(x,y)d1​(x,y)+dx​+dy​−2∗d′lca′(x,y)然後除個二。 在第二

2018.10.26 NOIP模擬 圖最小生成樹+線段合併

傳送門 首先最開始說的那個一條路徑的權值就是想告訴你兩個點之間的貢獻就是瓶頸邊的權值。 那麼肯定要用最小生成樹演算法。 於是我考場上想了30min+30min+30min+的樹形dpdpdp 發現轉移是

naive的圖線段合併

Preface Debug了半個晚上,有必要總結一下。 今天打了兩三個線段樹合併的題,深深感到資料結構對手殘黨的惡意。動輒上百行的程式碼,一定要非常熟練才有可能在真正比賽的時候打出來。 題意 有一張無向圖,nnn個點,mmm條邊,常數LLL。有點權(記為c[i]

HDU 5649 DZY Loves Sorting二分答案+線段線段合併+線段分割

題意 一個 \(1\) 到 \(n\) 的全排列,\(m\) 種操作,每次將一段區間 \([l,r]\) 按升序或降序排列,求 \(m\) 次操作後的第 \(k\) 位。 \(1 \leq n \leq 10^5\) 思路 兩個 \(\log\) 的做法展現了二分答案的強大功能。首先二分列舉第 \(k

HDU 5649 DZY Loves Sorting二分答案+線段線段合並+線段分割

空間 namespace memset ons \n create name 題意 size 題意 一個 \(1\) 到 \(n\) 的全排列,\(m\) 種操作,每次將一段區間 \([l,r]\) 按升序或降序排列,求 \(m\) 次操作後的第 \(k\) 位。 \(1

LOJ #2359. 「NOIP2016」天天愛跑步倍增+線段合併

題意 題解 考慮把一個玩家的路徑 \((x, y)\) 拆成兩條,一條是 \(x\) 到 \(lca\) ( \(x, y\) 最近公共祖先) 的路徑,另一條是 \(lca\) 到 \(y\) 的路徑。(對於 \(x, y\) 是 \(lca\) 的情況需要特殊考慮一下就行了) 這個求 \(lca\) 的過

uoj#418. 【集訓隊作業2018】三角形線段合併

傳送門 好迷啊……膜一下ljz 考慮每個操作,如果把操作按先後順序放到序列上的話,操作一就是把\(w_i\)的石子放到某個節點,那麼就是在序列末端加入\(w_i\),然後根據貪心肯定要把它所有兒子的石子拿走,也就是要減去\(\sum w_{son}\) 那麼每個點的答案就是序列的最大字首 因為父親節點

【BZOJ2212】【POI2011】Tree Rotations線段合併

Description Solution 對於每個節點有一棵權值線段樹,向上遞迴時合併同時計算逆序對即可。 Source /*****************************