1. 程式人生 > 實用技巧 >sql注入(報錯注入)

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
using
namespace 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;
}