sql注入(報錯注入)
一、引入
有的時候,我們不僅需要支援修改,還需要支援訪問歷史版本。
這個時候普通的線段樹就沒法勝任了,因為每次我們都覆蓋了之前的版本。
若想知道資料集在任意時間的歷史狀態,有沒有什麼方法呢?
方法一:直接記錄之前得到的所有的線段樹。在第 i 項操作結束後(∀i∈[1,M]),把整個線段樹拷貝一遍,儲存在 history[i] 中,多耗費 M 倍的空間。
複雜度 O(n2)。
方法二:注意到每次修改的位置都不會很多,所以相同的節點就沒必要再記錄一遍了。
複雜度 O(n log n)。
比如說要對 15 號節點進行“單點修改”,我們需要新建節點,如下圖所示,白色的為最初版本的線段樹,紅色的為版本 2。產生了 O(logN) 個新節點。
唯一的問題是由於需要每次新建節點,我們沒辦法再用位運算(p<<1,p<<1|1)訪問子節點,而需要在每一個點上記錄左右兒子的位置。
這就是可持久化線段樹(主席樹)的基本思想。
二、區間第 k 小
題目大意:Luogu P3834主席樹模板題。
長度為 n 的陣列,每次查詢一段區間裡第 k 小的數。1≤n,q≤2×105。
Solution:
我們先考慮一個比較簡單的問題:如何維護全域性第 k 小?
維護序列中落在值域區間 [L,R] 中數的個數(記作 cntL,R)。比較 cntL,mid與 k 的大小關係,即可確定序列中第 k 小的數是 ≤mid 還是 >mid,從而可以進入線段樹的左、右子樹之一。換言之,可以建一棵權值線段樹,然後線上段樹上二分解決。
維護字首第 k 大?
把這個權值線段樹可持久化,這樣我們就可以隨時拎出來一個字首。
區間第 k 大?相當於兩個線段樹相減(類似字首和?),同樣可以用可持久化線段樹維護。
“root[r] 的值域區間 [L,R] 的 cnt 值”-“root[l-1] 的值域區間 [L,R] 的 cnt 值”=“序列中落在值域 [L,R] 內的數的個數”,也就是可持久化線段樹中兩個代表相同值域的節點具有可減性。
時間複雜度 O(n log n)。
#include<bits/stdc++.h> #define int long long usingnamespace std; const int N=2e5+5; int n,m,l,r,k,tot,a[N],t,b[N],lc[N<<5],rc[N<<5],sum[N<<5],root[N]; //lc[],rc[]:左右子節點編號 tot:可持久化線段樹的總點數 root[]:可持久化線段樹的每個根 void build(int &p,int l,int r){ //建出一棵初始時的的樹 p=++tot,sum[p]=0; //新建一個節點 if(l==r) return ; int mid=(l+r)/2; build(lc[p],l,mid); build(rc[p],mid+1,r); } int insert(int p,int l,int r,int pos,int ave){ int x=++tot; lc[x]=lc[p],rc[x]=rc[p],sum[x]=sum[p]; //動態開點,先複製原來的節點 if(l==r){sum[x]+=ave;return x;} int mid=(l+r)/2; if(pos<=mid) lc[x]=insert(lc[p],l,mid,pos,ave); else rc[x]=insert(rc[p],mid+1,r,pos,ave); sum[x]=sum[lc[x]]+sum[rc[x]]; return x; } int query(int x,int y,int l,int r,int k){ //在 x,y 兩個節點上,值域為 [l,r],求第 k 小的數 if(l==r) return l; //找到答案 int mid=(l+r)/2,v=sum[lc[x]]-sum[lc[y]],ans=0; //v:有多少個數落在值 [l,mid] 內 if(v>=k) ans=query(lc[x],lc[y],l,mid,k); else ans=query(rc[x],rc[y],mid+1,r,k-v); return ans; } signed main(){ scanf("%lld%lld",&n,&m); for(int i=1;i<=n;i++) scanf("%lld",&a[i]),b[++t]=a[i]; sort(b+1,b+1+t),t=unique(b+1,b+1+t)-b-1; //離散化 build(root[0],1,t); for(int i=1;i<=n;i++){ int x=lower_bound(b+1,b+1+t,a[i])-b; root[i]=insert(root[i-1],1,t,x,1); //在上一個版本的基礎上修改 } while(m--){ scanf("%lld%lld%lld",&l,&r,&k); int x=query(root[r],root[l-1],1,t,k); printf("%lld\n",b[x]); } return 0; }
三、樹上第 k 小
題目大意:Luogu P2633Count on a tree。
n 個點的一棵樹,每次查詢一條鏈 u,v 上第 k 小的數。1≤n,q≤105。
Solution:
樹鏈剖分,然後可持久化,每次拿出來 O(log n) 個線段樹進行二分,……
恭喜你想到了一個 O(n log3 n) 的演算法。
我們注意到,在可持久化線段樹上,我們的 root[x] 不一定去依賴 root[x-1],完全可以依賴別的位置。
所以我們可以每一個點依賴它的父節點。這樣每一個點的線段樹就是維護的它到根節點的資訊。
對於一條鏈 u...v,我們設 u 和 v 的 LCA 是 d,那麼只需要在 T(u)+T(v)-T(d)-T(fa(d)) 上進行二分即可。
時間複雜度 O(n log n)。
#include<bits/stdc++.h> #define int long long using namespace std; const int N=1e5+5; int n,m,x,y,k,lastans,tot,a[N],t,b[N],lc[N<<5],rc[N<<5],sum[N<<5],root[N],cnt,hd[N],to[N<<1],nxt[N<<1],dep[N],f[N][30]; void add(int x,int y){ to[++cnt]=y,nxt[cnt]=hd[x],hd[x]=cnt; } void build(int &p,int l,int r){ p=++tot,sum[p]=0; if(l==r) return ; int mid=(l+r)/2; build(lc[p],l,mid); build(rc[p],mid+1,r); } int insert(int p,int l,int r,int pos,int ave){ int x=++tot; lc[x]=lc[p],rc[x]=rc[p],sum[x]=sum[p]; if(l==r){sum[x]+=ave;return x;} int mid=(l+r)/2; if(pos<=mid) lc[x]=insert(lc[p],l,mid,pos,ave); else rc[x]=insert(rc[p],mid+1,r,pos,ave); sum[x]=sum[lc[x]]+sum[rc[x]]; return x; } int query(int a,int b,int c,int d,int l,int r,int k){ if(l==r) return l; int mid=(l+r)/2,v=sum[lc[a]]+sum[lc[b]]-sum[lc[c]]-sum[lc[d]],ans=0; //T(u)+T(v)-T(d)-T(fa(d)) if(v>=k) ans=query(lc[a],lc[b],lc[c],lc[d],l,mid,k); else ans=query(rc[a],rc[b],rc[c],rc[d],mid+1,r,k-v); return ans; } void dfs(int x,int fa){ //預處理 root[x]=insert(root[fa],1,t,lower_bound(b+1,b+1+t,a[x])-b,1); //每一個點依賴它的父節點 dep[x]=dep[fa]+1; for(int i=0;i<=19;i++) f[x][i+1]=f[f[x][i]][i]; for(int i=hd[x];i;i=nxt[i]){ int y=to[i]; if(y!=fa) f[y][0]=x,dfs(y,x); } } int LCA(int x,int y){ //求 LCA if(dep[x]<dep[y]) swap(x,y); for(int i=20;i>=0;i--){ if(dep[f[x][i]]>=dep[y]) x=f[x][i]; if(x==y) return x; } for(int i=20;i>=0;i--) if(f[x][i]!=f[y][i]) x=f[x][i],y=f[y][i]; return f[x][0]; } signed main(){ scanf("%lld%lld",&n,&m); for(int i=1;i<=n;i++) scanf("%lld",&a[i]),b[++t]=a[i]; for(int i=1;i<n;i++){ scanf("%lld%lld",&x,&y); add(x,y),add(y,x); } sort(b+1,b+1+t),t=unique(b+1,b+1+t)-b-1; //離散化 build(root[0],1,t),dfs(1,0); while(m--){ scanf("%lld%lld%lld",&x,&y,&k),x^=lastans; int d=LCA(x,y),v=query(root[x],root[y],root[d],root[f[d][0]],1,t,k); printf("%lld\n",lastans=b[v]); } return 0; }
四、可持久化並查集
並查集的基本操作:
int f[N],sz[N]; //fa、size int find(int x){ //查詢 return x==f[x]?x:f[x]=find(f[x]); //優化 1:路徑壓縮 } void merge(int x,int y){ //合併 x=find(x),y=find(y); if(x!=y){ if(sz[x]<sz[y]) swap(x,y); f[y]=x,sz[x]+=sz[y]; } //優化 2:啟發式合併(按秩合併) }
首先科普一個關於並查集的知識點:
- 按秩合併 + 路徑壓縮:O(α(n))(反阿克曼函式)
- 只用按秩合併或只用路徑壓縮:O(log n)
在某些情況下,我們只能用按秩合併不能用路徑壓縮,比如可持久化。
然後迴歸正題。
題目大意:Luogu P3402 可持久化並查集模板題。
實現一個可持久化並查集,不光要支援所有並查集的操作,還需要支援訪問歷史版本。1≤n,q≤105。
Solution:
用可持久化線段樹,我們可以實現陣列的可持久化。也就是維護一個數組,支援單點修改陣列元素和訪問歷史版本。
並查集,其實無非是維護 fa 和 size,將這兩個陣列都可持久化,我們就可以實現並查集的可持久化。
不能路徑壓縮,因為那樣的話 fa 會進行很多修改。
時間複雜度 O(n log2 n),兩個 log n 一個來自按秩合併並查集一個來自可持久化線段樹。
#include<bits/stdc++.h> #define int long long using namespace std; const int N=1e5+5; int n,m,opt,x,y,tot,a[N],lc[N<<5],rc[N<<5],val[N<<5],root[N],sz[N<<5],fa[N<<5]; void build(int &p,int l,int r){ p=++tot; if(l==r){fa[p]=l;return ;} //初始版本:父親是它自己 int mid=(l+r)/2; build(lc[p],l,mid); build(rc[p],mid+1,r); } int modify(int p,int l,int r,int pos,int ave){ //把 pos 的父親改成 ave int x=++tot; lc[x]=lc[p],rc[x]=rc[p]; if(l==r){fa[x]=ave,sz[x]=sz[p];return x;} int mid=(l+r)/2; if(pos<=mid) lc[x]=modify(lc[p],l,mid,pos,ave); else rc[x]=modify(rc[p],mid+1,r,pos,ave); return x; } int query(int p,int l,int r,int pos){ //詢問某一個版本的一個點的父親 if(l==r) return p; int mid=(l+r)/2,ans=0; if(pos<=mid) ans=query(lc[p],l,mid,pos); else ans=query(rc[p],mid+1,r,pos); return ans; } void add(int p,int l,int r,int pos){ if(l==r){sz[p]++;return ;} int mid=(l+r)/2; if(pos<=mid) add(lc[p],l,mid,pos); else add(rc[p],mid+1,r,pos); } int find(int p,int v){ int x=query(p,1,n,v); //查詢在版本 p 中點 v 的父親 return v==fa[x]?x:find(p,fa[x]); //無路徑壓縮 } signed main(){ scanf("%lld%lld",&n,&m); build(root[0],1,n); for(int i=1;i<=m;i++){ scanf("%lld",&opt); if(opt==1){ scanf("%lld%lld",&x,&y); root[i]=root[i-1],x=find(root[i],x),y=find(root[i],y); if(fa[x]!=fa[y]){ if(sz[x]<sz[y]) swap(x,y); root[i]=modify(root[i-1],1,n,fa[y],fa[x]); //按秩合併,小的往大的合併 if(sz[x]==sz[y]) add(root[i],1,n,fa[x]); //小的增加 size } } else if(opt==2) scanf("%lld",&x),root[i]=root[x]; else{ scanf("%lld%lld",&x,&y); root[i]=root[i-1],x=find(root[i],x),y=find(root[i],y); if(fa[x]==fa[y]) puts("1"); else puts("0"); } } return 0; }