數據結構
二叉搜索樹
遞歸定義
它或者是一棵空樹,或者是具有下列性質的二叉樹: 若它的左子樹不空,則左子樹上所有結點的值均小於它的根結點的值; 若它的右子樹不空,則右子樹上所有結點的值均大於它的根結點的值; 它的左、右子樹也分別為二叉搜索樹。
•維護一個集合,支持操作:插入、刪除、查找
•方法:建立一棵有序的二叉樹,每個結點對應集合中一個元素。
•滿足性質:結點u的左子樹的結點權值都比u小,右子樹的結點權值都比u大。
•插入、刪除、查找只需在二叉樹中按照權值往下走即可。
可能的問題:
可能退化成一條鏈(例如如果按升序插入),那么每次操作平均復雜度O(n)需要一些技巧讓二叉搜索樹保持平衡。STL中的set和map都是平衡的二叉搜索樹。noip一般情況下夠用。
線段樹
護區間信息的數據結構
每個結點對應一個序列的區間
完全二叉樹,左子樹為左一半區間,右子樹為右一半
單點/區間修改。
動態開點
例題
維護一個數據結構,支持以下兩種操作:
1.插入一個數x
2.查詢[l,r]區間中有多少個數
操作數<=10^5 x,l,r<=10^9
強制在線
用線段樹做
做法
注意雖然inf=10^9很大,log(inf)還是很小
操作數很少
每個結點的值表示對應區間內的數的個數
每次操作至多增加\(O(logn)\)個非零結點
我們只需要將這\(logn\)個非零結點開出來即可,空間復雜度\(O(nlog(inf))\)
查詢時,如果碰到沒有開出的結點,說明這個結點值為\(0\),返回\(0\)即可
注意此時不能使用數組模擬二叉樹的方法(和堆類似,\(1\)為根,\(2n-1\)和\(2n\)分別為n的左右兒子) 因為得開\(10^9\)的數組,爆空間
得加上左右兒子指針的數組。
為新開的結點賦予大於零的編號 \(o=++tot\);
查詢時遇到結點編號為\(0\),就說明這個結點為空,對應區間中沒有數
線段樹的合並
考慮都是動態開點的兩棵權值線段樹(結點權值為對應區間的數的個數),如何將這兩棵線段樹合並?(即將他們對應的兩個數集合並)
tree *merge(int l, int r, tree *A, tree *B){
if(A == NULL) return B;
if(B == NULL) return A;
if(l == r) return new tree(NULL, NULL, A -> data + B ->data);
int mid = (l + r) >> 1;
return new tree(merge(l, mid, A -> ls, B -> ls), merge(mid + 1, r, A -> rs, B -> rs), A -> data + B -> data);
}
復雜度:兩棵線段樹中重合結點(對應位置都開出來的結點)的個數。
一棵權值線段樹表示一個數集。
如果每棵權值線段樹對應的數集都只有一個數,那么這棵權值線段樹只有\(O(log(inf))\)個開出的結點
\(n\)個這樣的權值線段樹合並,復雜度?
將n個這樣表示單元素集合的權值線段樹合並,復雜度不會高於將n個元素插入權值線段樹。
因此復雜度\(O(nlog(inf))\)
線段樹優化建圖
以如下方式給定一個有向圖:
給出結點量n與參數m
接下來m行,每行三個整數\(l_i,r_i,w_i\)
表示點xi向\([l_i,r_i]\)的每個點連出一條長度為\(w_i\)的邊
求1到n點的最短路
\(n,m<=10^5\)
做法
對n個頂點建立一棵線段樹。
對於\(l_i,r_i,w_i\)
只需將\([l_i,r_i]\)分解成線段樹上\(log(n)\)個結點
\(x_i\)分別向這\(log(n)\)個結點連一條長度為\(w_i\)的有向邊即可
邊數降為\(O(mlogn)\)
例題
感覺線段樹的這些操作聽得一臉懵逼
並查集
按秩合並
記錄出\(size[i]\)表示這個集合的元素個數,合並時把元素個數小的合並到大的上
為什么要按秩合並?
眾所周知,並查集如果深度過深了,會導致查詢時復雜度退化所以就可以通過記錄\(size\)來減少深度
void join(int x,int y)
{
x=find(x),y=find(y);
if(size[x]<size[y]) f[x]=y,xize[y]+=size[x];
else f[y]=x,xize[x]+=size[y]
}
二分圖判定
黑白染色法
大致思路就是先找到一個沒被染色的節點u,把它染上一種顏色,之后遍歷所有與它相連的節點v,如果節點v已被染色並且顏色和節點u一樣,那么就失敗了。如果這個節點v沒有被染色,先把它染成與節點u不同顏色的顏色,然后遍歷所有與節點v相連的節點............就這樣循環下去,直到結束為止。
vector<int> q[N];
int col[N];//col[u]表示u的顏色,1為白色,-1為黑色,0為未染色
bool flag=true;
void dfs(int u,int c)
{
col[u]=c;
for(int i=0;i<e[u].size();++i)
{
int v=e[u][i];
if(!col[v]) dfs(v,-c);
else if(col[v]==c) flag=false;
}
}
for(int i=1;i<=n;++i)
if(!col[i]) dfs(i,1);
字典樹
之前寫的筆記鏈接
長成這個樣子
設字母集為\(\sum\),則每個結點有至多\(|\sum|\)個兒子
例:給定\(n\)個字符串的集合,\(q\)個詢問,每個詢問給出一個字符串,詢問集合中有沒有這個字符串
用\(trie\)做,設字符串總長度為L,空間復雜度\(O(L*|\sum|)\),時間復雜度\(O(L)\)
int rt=1,cnt=1;
int ch[N][27],sizes[N];//N是最長串
void ins(string s)
{
int o=rt;
for(int i=0;i<s.size();++i)
{
k=s[i]-'a';
if(ch[o][k]) o=ch[o][k];
else ch[o][k]=++cnt,o=ch[o][k];
}
sizes[o]++;
}
int query(string s)//查詢有沒有出現過
{
int o=rt;
for(int i=0;i<s.size();++i)
{
int k=s[i]-'a';
if(ch[o][k]) o=ch[o][k];
else return 0;
}
return 1;
}
用字典樹解決異或問題(01Trie)
給定n個正整數,求這\(n\)個整數中兩兩異或的最大值?
\(n<=10^5\),每個整數\(a_i<=10^9\)
把每個正整數轉換為一個\(30\)位的二進制串,分別插入一棵\(trie\)中
對於每一個整數X,我們想找到它與其它整數異或得到的最大值
沿着\(trie\)從上往下走,采取貪心策略。
每一步設X的當前位為\(b\),則優先往\(b^1\)方向走(這樣可以在結果的這一位得到一個1);如果\(b^1\)方向沒有兒子,則往\(b\)方向走
int ch[N*30][2];
int rt=1,cnt=1;
void ins(int x)
{
int o=rt;
for(int i=30;i>=0;--i)
{
int k=x>>i&1;
if(ch[o][k]) o=ch[o][k];
else ch[o][k]=++cnt,o=cnt;
}
}
int query(int x)
{
int ans=0;
int o=rt;
for(int i=30;i>=0;--i)
{
int k=x>>i & 1;
if(ch[o][k^1]) ans|=1<<i,o=ch[o][k^1];
else o=ch[o][k];
}
return ans;
}
int a[N],n;
for(int i=0;i<n;++i) scanf("%d",a+i);
for(int i=0;i<n;++i) ins(a[i]);
int ans=0;
for(int i=0;i<n;++i) ans=max(ans,query(a[i]));
字符串hash
string s;
usigned long long v=0;//v是s的hash值
usigned long long p=1;
for(int i=0;i<s.size();++i)
{
v+=p*(s[i]-'a');
p*=26ull;
}
雙hash
取兩個模數之后兩個模數的hash都出現過才行
進制hash
把每位看成進制,有效避免了\(ab,ba\)判斷不出來的情況
如何得到一個字符串所有子串的hash
給定一個字符串,每次詢問給定四個數\(l_1,r_1,l_2,r_2\),滿足\(r_1-l_1=r_2-l_2\)問
st表
令\(f[i][j]\)表示從i開始,\(2^j\)個數的最小值
\(f[i][j]\)總共有\(nlogn\)個狀態
可以由\(j\)從小到大遞推求出
\(f[i][j]=min(f[i][j-1],f[i+(1<<(j-1))][j-1]);\)
感覺這東西和倍增差不多
int a[N],f[N][20];
int lg[N];
void init()
{
for(int i=1;i<=n;++i) f[i][0]=a[i];
for(int j=1;(1<<j)<=n;++j)
for(int i=1;i+(1<<j)-1<=n;++i)
f[i][j]=min(f[i][j-1],f[i+(1<<j)][j-1]);
for(int j=1;(1<<j)<=n;++j) lg[(1<<j)]=j;
for(int j=1;j<=n;+j)
if(!lg[j]) lg[j]=lg[j-1];
}
int query(int l,int r)
{
int k=lg[r-l+1];
return min(f[l][k],f[l-(1<<k)+1][k]);
}
倍增
\(f[i][j]\)表示\(i\)的第\(2^i\)祖先
inr lca(int u,int v)
{
if(dep[u]>dep[v]) swap(u,v);
int k=dep[v]-dep[u];
for(int i=0;i<=lg[k];++i)
if(k>>1&1) v=f[i][k];
if(u==v) return u;
for(int i=lg[n];i>-0;--i)
if(f[u][i]!=f[v][i])
u=f[u][i],v=f[v][i];
return f[u][0];
}
lca轉換為RMQ問題
感覺比較難
int seq[N<<1],cnt,pos[N]; //dfs序
int f[N<<1][20],dep[N]; //f[i][j]表示dfs序中從i位置開始2^j個結點中 深度最小的結點
void init() {
for(int i=1;i<=cnt;++i) f[i][0]=seq[i];
for(int j=1;j<=LG[cnt];++j)
for(int i=1;i+(1<<j)-1<=cnt;++i)
f[i][j] = dep[f[i][j-1]]<dep[f[i+(1<<j-1)][j-1]]?f[i][j-1]:f[i+(1<<j-1)][j-1];
}
int lca(int u,int v) {
int a=pos[u],b=pos[v];
if(a>b) swap(a,b);
int k=LG[b-a+1];
return dep[f[a][k]]<dep[f[b-(1<<k)+1][k]]?f[a][k]:f[b-(1<<k)+1][k];
}
tarjan算法離線求lca
ector<int> e[N],Q[N],id[N]; //e是邊表,Q[i]中保存的是與i點相關的查詢
int f[N],n; //f並查集 已初始化
int ans[N];
bool vis[N]; //dfs時是否經過
int find(int x){return x==f[x]?x:f[x]=find(f[x]);}
void ins_query(int u,int v,int o) {
//插入一個編號為o的詢問,詢問(u,v)兩點的lca
Q[u].push_back(v),id[u].push_back(o);
Q[v].push_back(u),id[v].push_back(o);
}
void dfs(int u) {
vis[u]=true;
for(int i=0;i<Q[u].size();++i) {
int v=Q[u][i];
//id[u][i]表示lca查詢(u,v)的編號
if(vis[v]) ans[id[u][i]]=find(v);
}
for(int i=0;i<e[u].size();++i) {
int v=e[u][i];
if(vis[v]) continue;
dfs(v);
f[find(v)]=u;
}
}
一個復雜度的證明
\(n+\frac{n}{2}+\frac{n}{3}+\frac{n}{4}+......+\frac{n}{n}=n(1+\frac{1}{2}+\frac{1}{3}+......+\frac{1}{n})=n\times log(n)\)
非旋轉treap即可持久treap
那天沒事的時候來看看