算法學習心得


最近學(復習?)了很多省選算法,先把心得寫在這里,將來如果忘了拿來復習吧

一、樹鏈剖分

樹鏈剖分是處理一類在樹的一條鏈上修改、查詢最大/最小值/權值和的算法。效率nlog^2n,大概數據在3w到5w左右比較正常吧

樹鏈剖分不支持導致樹的形態發生改變的操作,比如插入/刪除一條邊

對於某一修改x到y路徑上的東西操作,正常的模擬做法是先提出x和y的lca,然后一步一步往上走處理x到lca和y到lca的路徑

樹鏈剖分的思想只是在這個基礎上用線段樹優化一步一步往上走的過程

 

1、無論是暴力還是正解,都要先把無根樹轉成有根樹

所以第一遍dfs1:無根樹轉成有根樹,先求出這些東西:

fa[i][j]:i節點往上走2^j步能走到的點,這一步主要是求lca用的

depth[i]:i節點的深度,還是lca用

son[i]:第i個節點下面的兒子數

 

2、接下來就要考慮怎樣用線段樹模擬一步一步往上走的過程

線段樹里存的是點權,如果題目給的是邊權的話就用這條邊指向的兒子節點的點權表示

為什么線段樹會快呢?因為如果往上走的時候如果上面的一條鏈是線段樹的一段連續的區間,那么可以直接logn提出我們需要的東西

但是不一定每次往上走的時候走的路徑都是線段樹的一段連續的區間,所以一開始點權加入線段樹的順序就很重要了

那么怎樣才能盡可能保證走的是連續的區間呢

引入輕邊與重邊:對於一個節點x,它下面可能連出很多邊,每條邊指向它的一個兒子

假設x的所有兒子中son[y]最大,那么連接(x,y)的邊是重邊,y是x的重兒子。其他x下面連出的邊都是輕邊,x的其他兒子也就是輕兒子

找到一個點之后,就直接先把重兒子加入線段樹,之后再依次處理其他輕兒子

分析一下這樣做的作用:找出兒子中son[y]最大的,相當於子樹大小最大的,那么操作經過這些點的概率最大

因此在線段樹中使x和y放在一起,那么x和y被一起訪問的概率比其他x和x的兒子z被一起訪問的概率大

有一個比較不太嚴格的結論:從x到lca的路徑大致要在線段樹中找logn次,每次logn,所以平均每次操作log^2n

我的寫法可能有點銼

首先還是要一個dfs2:算出每個點加入線段樹的順序

place[i]:i在線段樹中的位置

pplace[i]:place[i]的反操作,保存線段樹中位置是i的點

belong[i]:線段樹中找到i往上走能走到的最上面的一個點。換句話說,在線段樹中belong[i]和i之間的邊是連續的一段,但是belong[i]的父親節點和這一段是不連續的

然后一個buildtree建樹,葉節點的權就是v[pplace[k]]

對於詢問(from,to),在往上走的時候就可以這樣寫:

while (belong[from]!=belong[to])

{

   l=place[belong[from]];:線段數中能走到的最上面的點,belong[from]和from在一個連續的區間

   r=place[from];:注意加入線段樹的順序是從上往下的,因此belong[from]在from之前加入,lr千萬別搞混

   search_in_tree(1,l,r);:各種線段樹的查詢修改操作不用講了吧

   from=fa[belong[from]][0];:from到belong[from]都做完了,因此from直接跳到belong[from]的父親節點

}

  l=place[to];

  r=place[from];

  search_in_tree(1,l,r);:別忘了最后from和to在線段樹中連續了之后再搞一次

以[ZJOI2008]樹的統計為例,以下為核心代碼:

  1 inline void dfs1(int x,int dep)
  2 {
  3     if (mrk[x])return;
  4     mrk[x]=1;depth[x]=dep;son[x]=1;
  5     for(int i=1;i<=15;i++)
  6       fa[x][i]=fa[fa[x][i-1]][i-1];
  7     for (int i=head[x];i;i=e[i].next)
  8     if (!mrk[e[i].to])
  9     {
 10         fa[e[i].to][0]=x;
 11         dfs1(e[i].to,dep+1);
 12         son[x]+=son[e[i].to];
 13     }
 14 }
 15 inline void dfs2(int x,int chain)
 16 {
 17     int k=0,mx=0;
 18     place[x]=++tt;belong[x]=chain;
 19     pplace[tt]=x;
 20     for (int i=head[x];i;i=e[i].next)
 21       if (fa[x][0]!=e[i].to)
 22       {
 23         if (son[e[i].to]>mx)
 24         {
 25             mx=son[e[i].to];
 26             k=e[i].to;
 27         }
 28       }
 29     if(!k)return;
 30     dfs2(k,chain);
 31     for(int i=head[x];i;i=e[i].next)
 32       if (e[i].to!=k&&e[i].to!=fa[x][0])
 33         dfs2(e[i].to,e[i].to);
 34 }
 35 inline void update(int k)
 36 {
 37     tree[k].mx=max(tree[k<<1].mx,tree[k<<1|1].mx);
 38     tree[k].tot=tree[k<<1].tot+tree[k<<1|1].tot;
 39 }
 40 inline void buildtree(int now,int l,int r)
 41 {
 42     tree[now].l=l;tree[now].r=r;
 43     if (l==r)
 44     {
 45         tree[now].mx=v[pplace[l]];
 46         tree[now].tot=v[pplace[l]];
 47         return;
 48     }
 49     int mid=(l+r)>>1;
 50     buildtree(now<<1,l,mid);
 51     buildtree(now<<1|1,mid+1,r);
 52     update(now);
 53 }
 54 inline int LCA(int a,int b)
 55 {
 56     if (depth[a]<depth[b])swap(a,b);
 57     int res=depth[a]-depth[b];
 58     for (int i=0;i<=15;i++)
 59       if (res & (1<<i))a=fa[a][i];
 60     for (int i=15;i>=0;i--)
 61       if (fa[a][i]!=fa[b][i])
 62       {
 63         a=fa[a][i];
 64         b=fa[b][i];
 65       }
 66     if(a==b)return a;
 67     return fa[a][0];
 68 }
 69 inline int ask_in_tree(int now,int x,int y)
 70 {
 71     int l=tree[now].l,r=tree[now].r;
 72     if (l==x&&y==r)return tree[now].mx;
 73     int mid=(l+r)>>1;
 74     if (y<=mid)return ask_in_tree(now<<1,x,y);
 75     else if (x>mid)return ask_in_tree(now<<1|1,x,y);
 76     return max(ask_in_tree(now<<1,x,mid),ask_in_tree(now<<1|1,mid+1,y));
 77 }
 78 inline int sum_in_tree(int now,int x,int y)
 79 {
 80     int l=tree[now].l,r=tree[now].r;
 81     if (l==x&&y==r)return tree[now].tot;
 82     int mid=(l+r)>>1;
 83     if (y<=mid)return sum_in_tree(now<<1,x,y);
 84     else if (x>mid)return sum_in_tree(now<<1|1,x,y);
 85     return sum_in_tree(now<<1,x,mid)+sum_in_tree(now<<1|1,mid+1,y);
 86 }
 87 inline int ask(int from,int to)
 88 {
 89     int l,r,mx=-inf;
 90     while (belong[from]!=belong[to])
 91     {
 92         l=place[belong[from]];
 93         r=place[from];
 94         mx=max(mx,ask_in_tree(1,l,r));
 95         from=fa[belong[from]][0];
 96          
 97     }
 98     l=place[to];
 99     r=place[from];
100     mx=max(mx,ask_in_tree(1,l,r));
101      
102     return mx;
103 }
104 inline int sum(int from,int to)
105 {
106     int l,r;
107     int s=0;
108     while (belong[from]!=belong[to])
109     {
110         l=place[belong[from]];
111         r=place[from];
112         s+=sum_in_tree(1,l,r);
113         from=fa[belong[from]][0];
114     }
115     l=place[to];
116     r=place[from];
117     s+=sum_in_tree(1,l,r);
118     return s;
119 }
120 inline void change(int now,int x,int dat)
121 {
122     int l=tree[now].l,r=tree[now].r;
123     if (l==r)
124     {
125         tree[now].mx=tree[now].tot=dat;
126         return;
127     }
128     int mid=(l+r)>>1;
129     if (x<=mid)change(now<<1,x,dat);
130     else change(now<<1|1,x,dat);
131     update(now);
132 }
count

 

 

二、莫隊算法

首先,orz hzwer

莫隊算法是離線處理不帶修改的區間詢問的算法,效率nsqrt(n)

其實說到底也就是先對詢問按一種順序排個序,然后直接模擬處理的算法。但是因為是按照一定順序做詢問,所以有辦法證明算法復雜度是nlogn

首先,把n個元素分為sqrt(n)塊,每塊有sqrt(n)個元素。然后我們以詢問的左端點所在的塊的編號為第一關鍵字,以右端點為第二關鍵字排序。

一開始我們用兩個指針l=1,r=0來表示當前我們已經記錄的區間。然后按照排完的順序依次處理詢問。

bzoj3781為例:

題意是給定一個序列a,每次對於詢問[l,r],輸出Σc[i]^2,其中c[i]表示在區間[l,r]中i出現的次數

那么先考慮模擬的做法:

當前我們做到了[x,y],現在已知在區間[x,y]中的c[i],以及ans=Σc[i]^2。那么如何可以從c[i]以及x,y,ans繼續轉移呢?

我們現在要求[x,y+1]的ans了。那么[x,y+1]的答案比[x,y]多的就是a[y+1]對整個區間的貢獻。只要加上a[y+1]帶來的貢獻就可以了。

只需要y++   -->   ans-=c[y]^2   -->   c[y]++   -->  ans+=c[y]^2

就可以做到從區間[x,y]轉移到[x,y+1]了。

那么如果我們現在要求[x,y-1]的ans,只需在原來的ans中扣掉a[y]帶來的貢獻。

只需要ans-=c[y]^2   -->   c[y--]   -->   ans+=c[y]^2   -->   y--

就可以從[x,y]轉移到[x,y-1]

那么[x+1,y]和[x-1,y]就是同理了

這樣我們就可以從[x,y]轉移到任意的[x',y']了

但是為什么按着這樣的順序模擬就不會T呢?

在此引用黃巨大的話:

考慮第i個詢問和第i+1個詢問之間的關系:

一、i與i+1在同一塊內,r單調遞增,所以r是O(n)的。由於有n^0.5塊,所以這一部分時間復雜度是n^1.5。
二、i與i+1跨越一塊,r最多變化n,由於有n^0.5塊,所以這一部分時間復雜度是n^1.5
三、i與i+1在同一塊內時變化不超過n^0.5,跨越一塊也不會超過2*n^0.5,不妨看作是n^0.5。由於有n個數,所以時間復雜度是n^1.5
於是就變成了O(n^1.5)了

以下給出我寫的bzoj3781的核心代碼

 1 int n,m,l=1,r=0,k,sqrtn,ans;
 2 int rep[50010];
 3 struct query{
 4     int l,r;
 5     int from,rnk,ans;
 6 }q[50010];
 7 int s[50010],a[50010];
 8 bool operator <(const query &a,const query &b)
 9 {return a.from<b.from||a.from==b.from&&a.r<b.r;}
10 inline void solve(int x)
11 {
12     while (l<q[x].l){ans-=rep[a[l]]*rep[a[l]];rep[a[l]]--;ans+=rep[a[l]]*rep[a[l]];l++;}
13     while (l>q[x].l){l--;ans-=rep[a[l]]*rep[a[l]];rep[a[l]]++;ans+=rep[a[l]]*rep[a[l]];}
14     while (r<q[x].r){r++;ans-=rep[a[r]]*rep[a[r]];rep[a[r]]++;ans+=rep[a[r]]*rep[a[r]];}
15     while (r>q[x].r){ans-=rep[a[r]]*rep[a[r]];rep[a[r]]--;ans+=rep[a[r]]*rep[a[r]];r--;}
16     q[x].ans=ans;
17 }
18 int main()
19 {
20     n=read();m=read();k=read();
21     sqrtn=sqrt(n);
22     for (int i=1;i<=n;i++)a[i]=read();
23     for (int i=1;i<=m;i++)
24     {
25         q[i].l=read();q[i].r=read();
26         q[i].from=(q[i].l-1)/sqrtn+1;
27         q[i].rnk=i;
28     }
29     sort(q+1,q+m+1);
30     for (int i=1;i<=m;i++)
31       solve(i);
32     for (int i=1;i<=m;i++)
33     s[q[i].rnk]=q[i].ans;
34     for (int i=1;i<=m;i++)
35       printf("%d\n",s[i]);
36 }
bzoj3781

 三、2-sat算法

2-sat是處理關於二元組中兩個元素之間有特殊關系(比如不能同時取,不能同時不取,等等)的一種算法

首先推薦一篇講2-sat問題挺詳細的文章 

這篇真的很好

黃巨大看了也說好

沒有什么一串又一串的公式的,全都是講解

不過copy別人的勞動成果就不太好了吧……

只發超鏈接了:http://blog.csdn.net/jarjingx/article/details/8521690

 總之……按着我自己的理解再寫一遍

首先,假設存在這樣的關系:如果要取A必須取B,那么A到B連邊。

但是注意到“取”的狀態是不能直接轉移的,反而“不取”的狀態可以轉移。

換句話說,如果當前這個點取了,那么接下來所有還能取的點不能唯一確定狀態,反而那些不能取的就能確定了。

所以把圖反建,那么一條B到A的邊就表示取A必須取B,即不取B就不能取A。

先tarjan縮點,然后拓撲排序。按照上面所說,B到A的邊就表示不取B就不能取A,所以就可以轉移“不取”的狀態了

為什么要拓撲排序?這個問題我還是靠直觀感覺:邊x->y可以看成一種相對於y的限制,那么顯然入度越小限制越少,入度為0就沒有限制了。當然沒限制的先做啊

 四、后綴數組

明天省選一試……在退役之前把我會的都寫在這里吧……也許以后很少再接觸OI了……為什么莫名的傷感

后綴數組(SA),是一類處理字符串問題的算法。nlogn的時間效率,常數也還不算太大,它的應用還是很廣的。

顧名思義,它就是比較一個字符串的所有后綴的字典序大小的算法。

有兩種算法:一種是基於倍增思想的做法,另一種就是傳說中的DC3。這里介紹倍增法

假設我們有字符串S,要把S的所有后綴根據字典序排序,最簡單的比較方法當然是把n個后綴直接快排。但是有一個很嚴重的問題是,我們需要做nlogn次比較,但是和數字的比較不同,字符串的比較不是O(1)的,而是O(n)的。這樣總復雜度是n^2logn,使我們無法接受的。

那么怎樣才能更快算出字典序呢

很顯然SA這樣基於比較大小的算法時間復雜度一般不會少於nlogn。分析一下剛才的快排,就能發現瓶頸在於比較后綴的大小的時候太慢。

而倍增法就是很好的利用了字符串后綴的一些性質,能在O(1)的時間內完成比較。

我們用suffix[i]表示從i開始的S的這個后綴,即S[i]~S[n]

這時候開始我們就要用到倍增思想了

我們每次將后綴的前2^k個字符先提出來排序,先不管怎么做,只要2^k>n,那么我們就能得到要求的排名了

定義sa數組,sa[k][i]=x,表示當前我們排序后綴的前2^k的字符,當前排名第i小的后綴是suufix[x]。相對應的,定義逆數組rnk,rnk[k][i]=x,表示當前第suffix[i]的排名是x。那么只要知道sa就能算出rnk,只要知道rnk就能算出sa

顯然sa[1]和rnk[1]是很好預處理的。需要桶排一下

ps:這里桶排也是有特殊意義的,桶排保存i字母出現的次數的前綴和v[i],然后每次出現一個s[i],就讓rnk[v[s[i]]--]=i。這樣正確性顯然,而且保證了越前面的后綴的rnk越大

考慮當前我們做完了后綴的前2^(k-1)個字符的排序,現在要根據sa[k-1][...]和rnk[k-1][...]算出sa[k]和rnk[k]。

比如一個串abacbba,當前我們排完了后綴前2^1也就是前2個字母的順序,現在算前2^2也就是前4個的順序。

排序應當是這樣的

    a ab ac ba ba bb cb

sa 7 1  3  6   2   5   4

rnk2 4 3  6   5   4   1

顯然如果存在rnk[k][i]>rnk[k][j],那么隨着k的增大,rnk[k][i]也會一直大於rnk[k][j]。一旦符號定了,就不可能再改了。這個很好yy

現在考慮比較rnk相同的suffix[2]和suffix[6]

注意到所有長度為2的后綴都已經比較過了

那么實際上我們比較s[2~5]和s[6~7]

即是比較s[2~3]+s[4~5]和s[6~7]+“空集”

也就是說長度為2^k的后綴,即是兩個長度為2^(k-1)的后綴拼起來。而長度為2^(k-1)的后綴我們又都已經算出來了,那么比較大小就是O(1)的啦!

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM