可持久化線段樹+主席樹+動態主席樹


 

可持久化線段樹

 

整體還是很容易理解的,網上的教程都挺不錯,所以只簡單介紹下

可持久化的原理在於,借用已經建過的線段樹的一部分

比如,我們有一個數列$a=\{12,23,34,45,56,67,78,89\}$

而我們想要帶修改的維護這個數列中$[L,R]$的區間和

建一顆正常的、維護$a_1$~$a_8$區間和的線段樹就能解決了,這樣就是不修改的情況

 

問題在於,如果想在這個的基礎上維護歷史版本,應當如何處理?

假設第一次修改,將$a_3$改為$90$

如果我們據此重新建立一顆線段樹,可以發現,只有很少的節點跟初始的線段樹有出入

如果說的更加確切,有出入的節點為被修改點及其所有祖先

所以,我們建立一顆新的線段樹,相當於向某個歷史版本插入一條長度為logN的鏈

而對於這條鏈,每個節點的一個兒子一定指向一個沒有出入的區間(即之前某個歷史版本的節點)、另一個一定指向一個包含點修改的區間(新創建的節點),分開操作一下就行了

這樣,$M$次操作時,整體的時空消耗是$O(N+MlogN)$

模板題:洛谷P3919

雖然是可持久化數組,但是稍微修改一下(把修改和查詢換成區間)就是可持久化線段樹了

(注釋的是自己一開始犯的兩個錯誤)

#include <cstdio>
#include <cstring>
#include <cmath>
#include <iostream>
using namespace std;

const int MAX=1000005;

int sz=1,cnt=0;

struct Node
{
    int val,l,r;
    Node()
    {
        val=l=r=0;
    }
}t[MAX*25];

int n,m;
int a[MAX];
int id[MAX];

void Build()
{
    id[1]=1;
    for(int i=1;i<sz;i++)
        t[i].l=(i<<1),t[i].r=(i<<1)+1;
    for(int i=1;i<=n;i++)
        t[i+sz-1].val=a[i];
    cnt=(sz<<1)-1;
}

inline void Modify(int k,int x,int l,int r,int y)
{
    if(l==r)
    {
        t[cnt].val=y;//#2: Mistake t[cnt] as t[k]
        return;
    }
    
    int mid=(l+r)>>1;
    if(x<=mid)
    {
        t[cnt].r=t[k].r;
        t[cnt].l=++cnt;
        Modify(t[k].l,x,l,mid,y);
    }
    else
    {
        t[cnt].l=t[k].l;
        t[cnt].r=++cnt;
        Modify(t[k].r,x,mid+1,r,y);
    }
}

inline int Query(int k,int x,int l,int r)
{
    if(l==r)
        return t[k].val;
    int mid=(l+r)>>1;
    return (x<=mid?Query(t[k].l,x,l,mid):Query(t[k].r,x,mid+1,r));
}

int main()
{
//    freopen("input.txt","r",stdin);
    scanf("%d%d",&n,&m);
    for(int i=1;i<=n;i++)
        scanf("%d",&a[i]);
    while(sz<n)
        sz<<=1;
    
    Build();
    id[0]=1;//#1: Missing initial situation
    
    for(int i=1;i<=m;i++)
    {
        int ver,op,x,y;
        scanf("%d%d",&ver,&op);
        id[i]=++cnt;
        
        if(op==1)
        {
            scanf("%d%d",&x,&y);
            Modify(id[ver],x,1,sz,y);
        }
        else
        {
            scanf("%d",&x);
            t[cnt]=t[id[ver]];
            printf("%d\n",Query(id[ver],x,1,sz));
        }
    }
    return 0;
}
View Code

 


 

重點還是真正的應用...比如

主席樹

 

主席樹又叫函數式線段樹,可以解決的一種問題是 動態區間第$k$小

就是這道經典題:POJ 2104

網上有些博客在介紹的一開始講的太本質了,導致反而有點難理解

***注意:線段樹外的區間指的就是元素位置的區間,而線段樹內的區間指的是元素離散化后的數值的區間***

我們先考慮,如何通過線段樹,知道一個固定數列中第$k$小的數是多少【雖然這里的做法顯得很笨,但是是主席樹的簡化版本

我們可以將整個數列先離散化,然后對區間中的每一個數進行統計

例如:數列$a=\{10,20,30,20,50,10,60,40\}$,離散化后得到$b=\{1,2,3,2,5,1,6,4\}$

對於數列內每一個離散化后的數,我們建立一個基於數值的 區間和線段樹 統計它的出現次數

($7$、$8$是用來占位的,可以無視)

這樣,我們可以通過類似二分的思想找到第$k$小,而線段樹的節點已經幫助我們將區間對半切分

假設我們想找區間第$7$小:

step 1: 區間$[1,4]$內的數一共出現了$6$次,所以我們可以直接進入另一區間$[5,8]$,並且找這個區間中的第$1$小

step 2: 區間$[5,6]$內的數一共出現了$2$次,所以$[5,8]$中的第$1$小一定也是$[5,6]$中的第$1$小

step 3: 區間$[5,5]$內的數一共出現了$1$次,所以$5$正是$[5,6]$中的第$1$小,即整個查詢區間中的第$7$小

 

有了這樣的鋪墊,我們可以考慮引入可持久化的部分了

對於詢問的某個區間$[L_i,R_i]$,我們就相當於在處理 只加入$L_i$到$R_i$的元素時候,像上面問題一樣的區間第$k$小

所以為什么主席樹叫做函數式線段樹:我們可以通過前綴區間的相減來表示任意區間

用人話說,我們將離散化后的數列$b$的$n$個元素依次加入線段樹中,進而產生$n+1$個歷史版本(第$0$個歷史版本是空線段樹,其余依次為對$[1,1],[1,2],...,[1,n]$內元素的數值統計而成的線段樹)

通過這個方法,我們就能表示區間$[L_i,R_i]$所產生的線段樹了:對於每個節點,用第$R_i$版本的數值減去第$L_i-1$版本的數值(原理同用前綴和求區間和)

於是成功轉化為了上面的問題

UPD:更新了一下代碼

#include <cstdio>
#include <vector>
#include <cstring>
#include <algorithm>
using namespace std;

const int N=200005;

int root[N];

struct ChairmanTree
{
    int tot;
    int ls[N*20],rs[N*20],cnt[N*20];
    
    ChairmanTree()
    {
        tot=0;
        memset(root,0,sizeof(root));
        memset(ls,0,sizeof(ls));
        memset(rs,0,sizeof(rs));
        memset(cnt,0,sizeof(cnt));
    }
    
    void init()
    {
        memset(root,0,sizeof(root));
        for(int i=1;i<=tot;i++)
            ls[i]=rs[i]=cnt[i]=0;
        tot=0;
    }
    
    void pushup(int k)
    {
        cnt[k]=cnt[ls[k]]+cnt[rs[k]];
    }
    
    void add(int k,int x,int a,int b)
    {
        if(a==b)
        {
            cnt[++tot]=cnt[k]+1;
            return;
        }
        
        int cur=++tot,mid=(a+b)>>1;
        if(x<=mid)
        {
            ls[cur]=cur+1,rs[cur]=rs[k];
            add(ls[k],x,a,mid);
        }
        else
        {
            ls[cur]=ls[k],rs[cur]=cur+1;
            add(rs[k],x,mid+1,b);
        }
        pushup(cur);
    }
    
    //在第k個版本插入x 
    void insert(int k,int x,int a,int b)
    {
        root[k]=tot+1;
        add(root[k-1],x,a,b);
    }
    
    //在區間(l,r]中1~x的個數 
    //記得詢問時l,r為root 
    int order(int l,int r,int x,int a,int b)
    {
        if(a==b)
            return cnt[r]-cnt[l];
        
        int mid=(a+b)>>1;
        if(x<=mid)
            return order(ls[l],ls[r],x,a,mid);
        else
            return cnt[ls[r]]-cnt[ls[l]]+order(rs[l],rs[r],x,mid+1,b);
    }
    
    //區間(l,r]中第k小的數(不存在返回-1) 
    //記得詢問時l,r為root 
    int kth(int l,int r,int k,int a,int b)
    {
        if(k>cnt[r]-cnt[l])
            return -1;
        if(a==b)
            return a;
        
        int lp=cnt[ls[r]]-cnt[ls[l]],mid=(a+b)>>1;
        if(lp>=k)
            return kth(ls[l],ls[r],k,a,mid);
        else
            return kth(rs[l],rs[r],k-lp,mid+1,b);
    }
};

int n,m;
int a[N];
vector<int> v;

ChairmanTree t;

int main()
{
    scanf("%d%d",&n,&m);
    for(int i=1;i<=n;i++)
    {
        scanf("%d",&a[i]);
        v.push_back(a[i]);
    }
    
    sort(v.begin(),v.end());
    v.resize(unique(v.begin(),v.end())-v.begin());
    
    for(int i=1;i<=n;i++)
        t.insert(i,lower_bound(v.begin(),v.end(),a[i])-v.begin()+1,1,v.size());
    
    for(int i=1;i<=m;i++)
    {
        int x,y,k;
        scanf("%d%d%d",&x,&y,&k);
        printf("%d\n",v[t.kth(root[x-1],root[y],k,1,v.size())]);
    }
    return 0;
}
View Code

模板(多測記得調用init(),其他見注釋)

#include <cstdio>
#include <cstring>
#include <algorithm>
using namespace std;

const int N=200005;

int root[N];

struct ChairmanTree
{
    int tot;
    int ls[N*20],rs[N*20],cnt[N*20];
    
    ChairmanTree()
    {
        tot=0;
        memset(root,0,sizeof(root));
        memset(ls,0,sizeof(ls));
        memset(rs,0,sizeof(rs));
        memset(cnt,0,sizeof(cnt));
    }
    
    void init()
    {
        memset(root,0,sizeof(root));
        for(int i=1;i<=tot;i++)
            ls[i]=rs[i]=cnt[i]=0;
        tot=0;
    }
    
    void pushup(int k)
    {
        cnt[k]=cnt[ls[k]]+cnt[rs[k]];
    }
    
    void add(int k,int x,int a,int b)
    {
        if(a==b)
        {
            cnt[++tot]=cnt[k]+1;
            return;
        }
        
        int cur=++tot,mid=(a+b)>>1;
        if(x<=mid)
        {
            ls[cur]=cur+1,rs[cur]=rs[k];
            add(ls[k],x,a,mid);
        }
        else
        {
            ls[cur]=ls[k],rs[cur]=cur+1;
            add(rs[k],x,mid+1,b);
        }
        pushup(cur);
    }
    
    //在第k個版本插入x 
    void insert(int k,int x,int a,int b)
    {
        root[k]=tot+1;
        add(root[k-1],x,a,b);
    }
    
    //在區間(l,r]中1~x的個數 
    //記得詢問時l,r為root 
    int order(int l,int r,int x,int a,int b)
    {
        if(a==b)
            return cnt[r]-cnt[l];
        
        int mid=(a+b)>>1;
        if(x<=mid)
            return order(ls[l],ls[r],x,a,mid);
        else
            return cnt[ls[r]]-cnt[ls[l]]+order(rs[l],rs[r],x,mid+1,b);
    }
    
    //區間(l,r]中第k小的數(不存在返回-1) 
    //記得詢問時l,r為root 
    int kth(int l,int r,int k,int a,int b)
    {
        if(k>cnt[r]-cnt[l])
            return -1;
        if(a==b)
            return a;
        
        int lp=cnt[ls[r]]-cnt[ls[l]],mid=(a+b)>>1;
        if(lp>=k)
            return kth(ls[l],ls[r],k,a,mid);
        else
            return kth(rs[l],rs[r],k-lp,mid+1,b);
    }
};
View Code

 


 

動態主席樹

 

上面是簡單的、在固定的數組上進行查詢的主席樹

如果在查詢的同時支持對數組的點修改,不就更加NB了嗎?

但是,這個功能的加入並不簡單...又是看了幾個博客強行理解了好久好久(有些講解對新手不是很友好orz)

首先說明一下,動態主席樹跟靜態主席樹在數據結構上已經有些差距了:動態主席樹說到底是線段樹套線段樹(外層可以簡化為樹狀數組),而靜態主席樹是重復利用的線段樹,兩者是有一定區別的

但是,動態主席樹用到了和靜態主席樹類似的實現思想,就是維護前綴和(元素出現次數的前綴和)

在上面的靜態主席樹中,我們使用了可持久化線段樹來維護元素,而每個前綴和是一顆線段樹:雖然不同歷史版本的線段樹節點之間有交叉以重復利用,但每個歷史版本都有唯一且獨立的根節點

這就有點像我們求數列的區間和了:對於一個靜態的數組$a_i$,我們先計算前綴和$pre_i=pre_{i-1}+a_i$,然后通過$pre_R-pre_{L-1}$來求$[L,R]$的區間和;但是如果想求一個帶修改的數組的區間和,必須使用高級數據結構,例如線段樹/樹狀數組

在這里也是相似的,只不過區間中的元素從簡單的數字變成了記錄數值出現次數的線段樹了

於是,我們可以考慮 外層是線段樹/樹狀數組、內層是記錄數值出現次數的區間和線段樹 這樣的結構

  • 外層維護的是元素位置的區間:如果我們想查詢$[L,R]$的第$k$小,我們首先找的是外層的對應$[1,R]$、$[1,L-1]$前綴和的幾段區間(外層的節點,就是內層線段樹的根節點)【外層的線段樹的作用,是為了幫助我們找到位置區間對應的幾顆內層線段樹
  • 內層維護的是數值的出現次數:每棵線段樹表示,在根節點對應的外層區間中,每個數值出現的次數

先不談直觀上是$O(N^2)$的空間消耗(默認已經以原數組為基礎初始化過了):后面會有辦法解決這個問題;考慮一下使用這樣結構的可行性

 

【修改】

如果將位置$p_i$的數$x$修改為$y$,我們在外層線段樹發現$p_i$的位置一共被$logN$個區間(節點)包含;同時,以每個節點為根節點的線段樹中,分別各有$logN$的節點的值被$x$、$y$影響

於是,對於外層每個包含$p_i$的節點,我們都應該在以其為根節點的內層線段樹中將數值$x$的次數$+1$、將數值$y$的次數$-1$,並一直更新到內層線段樹的根節點

這樣,一次修改的復雜度是$O((logN)^2)$級別的

【查詢】

如果外層是線段樹,對於每次區間$[L,R]$的查詢,我們都需要先在外層鎖定僅包含區間$[L,R]$的內層根節點,這組節點最多有$logN$個

然后我們就可以轉化為靜態主席樹的簡單版本了,只不過這棵線段樹的每個節點的數值 都是 以這組以節點為根的線段樹 相同位置的節點 的數值之和(或者說,我們把這組線段樹直接數值意義上的疊加在一起)

然后就是同上用類似二分的方法求區間第$k$小,就不再贅述了

如果外層是樹狀數組,對於每次查詢,我們都需要先在外層分別鎖定僅包含區間$[1,L-1]$、$[1,R]$的兩組節點,每組節點最多有$logN$個

但是疊加成一顆線段樹時,要減去$[1,L-1]$這組的疊加,加上$[1,R]$這組的疊加,后面還是一樣的求區間第$k$小

這樣,一次查詢的復雜度也是$O((logN)^2)$級別的

 

現在我們回到一開始的問題:如何解決爆炸的空間?

如果把內層線段樹的節點全部事先開好的話,就的確是$O(N^2)$的了;但事實上,我們一共能訪問到內層線段樹的多少節點呢?

每次修改(基於原始數組初始化相當於修改$N$次),同時間復雜度一樣,是$(logN)^2$級別的

每次查詢,仍然同時間復雜度一樣,是$(logN)^2$級別的【但是查詢並不會對任何內、外層節點帶來修改,所以沒有必要開點】

這樣一來,我們真正能訪問到的,一共也就$N\cdot (logN)^2$個內層線段樹的節點;剩下來的,想都不用想,全是$0$,對於我們的查詢並不會產生影響

所以,可以通過類似可持久化線段樹的動態開點解決

 

模板題:洛谷P2617 (比原出處數據更強,甚至直接卡掉$O(N\cdot (logN)^3)$的線段樹套平衡樹)

說一下對我的坑點...其實動態主席樹的實現在常數上是沒多大區別的(線段樹和樹狀數組差不多),我對着自己TLE的代碼、抱着別人AC的代碼,反反復復查了一天半都沒找到個所以然

然后,發現我的離散化用的是$map$...在每次修改的時候也是直接用的map來找到離散化后的數(修改時一共調用了$3N$次$map$:初始、加、減各$N$次)

將離散化的互相對應關系用數組重新存了下,時間就直接降到了原來的一半,也就是說:$map$的常數約等於一個$logN$

細思極恐,建議用vector離散化

不過在這里強烈推薦樹狀數組:在外層的各種定位可以直接通過加減$lowbit$的$for$循環完成,而線段樹需要遞歸

(又注釋了一些制杖bug)

#include <cstdio>
#include <cstring>
#include <cmath>
#include <cstdlib>
#include <map>
using namespace std;

const int MAX=100005;

int tot=1,sz=1;
int t[MAX*400],l[MAX*400],r[MAX*400];

int n,m;
int a[MAX];

inline int lowbit(int x)
{
    return x&(-x);
}

inline void Insert(int k,int p,int a,int b,int x)
{
    if(a==b)
    {
        t[k]+=x;
        return;
    }
    
    int mid=(a+b)>>1;
    if(p<=mid)
    {
        if(!l[k])
            l[k]=++tot;
        Insert(l[k],p,a,mid,x);
    }
    else
    {
        if(!r[k])
            r[k]=++tot;
        Insert(r[k],p,mid+1,b,x);
    }
    
    t[k]=t[l[k]]+t[r[k]];
}

inline void Add(int k,int p,int x)
{
    for(int i=k;i<=n;i+=lowbit(i))//#1: Need setting limits
        Insert(i,p,1,sz,x);
}

int idx;
map<int,int> mp;
int rev[MAX<<1];//#4: Forget to expand the size

void Build()
{
    while(tot<n)
        tot<<=1;
    for(int i=1;i<=n;i++)
        Add(i,a[i],1);
}

int lsz,rsz;
int vl[MAX],vr[MAX];

inline int Query(int a,int b,int x)
{
    if(a==b)
        return a;
    
    int mid=(a+b)>>1,sum=0;//#2: Counting left value
    for(int i=1;i<=lsz;i++)
        sum-=t[l[vl[i]]];
    for(int i=1;i<=rsz;i++)
        sum+=t[l[vr[i]]];
    
    if(sum>=x)//#3: Reverse the operator
    {
        for(int i=1;i<=lsz;i++)
            vl[i]=l[vl[i]];
        for(int i=1;i<=rsz;i++)
            vr[i]=l[vr[i]];
        return Query(a,mid,x);
    }
    else
    {
        for(int i=1;i<=lsz;i++)
            vl[i]=r[vl[i]];
        for(int i=1;i<=rsz;i++)
            vr[i]=r[vr[i]];
        return Query(mid+1,b,x-sum);
    }
}

inline void Locate(int x,int y)
{
    lsz=rsz=0;
    for(int i=x;i;i-=lowbit(i))
        vl[++lsz]=i;
    for(int i=y;i;i-=lowbit(i))
        vr[++rsz]=i;
}

char op[MAX];
int x[MAX],y[MAX],k[MAX];

int main()
{
//    freopen("input.txt","r",stdin);
    scanf("%d%d",&n,&m);
    for(int i=1;i<=n;i++)
        scanf("%d",&a[i]),mp[a[i]]=1;
    for(int i=1;i<=m;i++)
    {
        op[i]=getchar();
        while(op[i]!='C' && op[i]!='Q')
            op[i]=getchar();
        if(op[i]=='Q')
            scanf("%d%d%d",&x[i],&y[i],&k[i]);
        else
            scanf("%d%d",&x[i],&y[i]),mp[y[i]]=1;
    }
    
    for(map<int,int>::iterator it=mp.begin();it!=mp.end();it++)
        it->second=++idx,rev[idx]=it->first;
    while(sz<idx)
        sz<<=1;
    for(int i=1;i<=n;i++)
        a[i]=mp[a[i]];
    for(int i=1;i<=m;i++)
        if(op[i]=='C')
            y[i]=mp[y[i]];
    
    Build();
    
    for(int i=1;i<=m;i++)
        if(op[i]=='C')
        {
            Add(x[i],a[x[i]],-1);//#5: Mistake x[i] as i
            a[x[i]]=y[i];
            Add(x[i],a[x[i]],1);
        }
        else
        {
            Locate(x[i]-1,y[i]);
            printf("%d\n",rev[Query(1,sz,k[i])]);
        }
    return 0;
}
View Code

 


 

這樣,可持久化線段樹的概念就算是基本學完了(雖然動態主席樹關聯並沒有那么大)←說的好像其他可持久化數據結構就會了一樣

真正的難點是將可持久化的思想靈活運用到各種各樣刁鑽的題目當中

有時間的話再補些不錯的題目上來orz

這算是我正式開始學習數據結構的入門吧...雖然都是大佬們隨便玩的東西,我枯了

 

HDU 6703 ($array$,$2019CCPC$網絡選拔賽)

這道題主要用到了兩個轉化的思想

第一個是,選$[1,r]$中不出現的數,可以轉化為選$[r+1,n]$中出現的數(由於$a_i$為$1$到$n$的一個排列)

第二個是,將$a_i$加上$10^7$,相當於原來小於$n$的值對於任意$r$都能被選取,可以直接向后插入到主席樹上

選取不小於$k$的最小值,可以通過先查詢$k$的order(小於$k$的數的個數)、再查詢第$order+1$小值完成

#include <cstdio>
#include <cstring>
#include <algorithm>
using namespace std;

const int N=200005;

int root[N];

struct ChairmanTree
{
    int tot;
    int ls[N*20],rs[N*20],cnt[N*20];
    
    ChairmanTree()
    {
        tot=0;
        memset(root,0,sizeof(root));
        memset(ls,0,sizeof(ls));
        memset(rs,0,sizeof(rs));
        memset(cnt,0,sizeof(cnt));
    }
    
    void init()
    {
        memset(root,0,sizeof(root));
        for(int i=1;i<=tot;i++)
            ls[i]=rs[i]=cnt[i]=0;
        tot=0;
    }
    
    void pushup(int k)
    {
        cnt[k]=cnt[ls[k]]+cnt[rs[k]];
    }
    
    void add(int k,int x,int a,int b)
    {
        if(a==b)
        {
            cnt[++tot]=cnt[k]+1;
            return;
        }
        
        int cur=++tot,mid=(a+b)>>1;
        if(x<=mid)
        {
            ls[cur]=cur+1,rs[cur]=rs[k];
            add(ls[k],x,a,mid);
        }
        else
        {
            ls[cur]=ls[k],rs[cur]=cur+1;
            add(rs[k],x,mid+1,b);
        }
        pushup(cur);
    }
    
    //在第k個版本插入x 
    void insert(int k,int x,int a,int b)
    {
        root[k]=tot+1;
        add(root[k-1],x,a,b);
    }
    
    //在區間(l,r]中1~x的個數 
    //記得詢問時l,r為root 
    int order(int l,int r,int x,int a,int b)
    {
        if(a==b)
            return cnt[r]-cnt[l];
        
        int mid=(a+b)>>1;
        if(x<=mid)
            return order(ls[l],ls[r],x,a,mid);
        else
            return cnt[ls[r]]-cnt[ls[l]]+order(rs[l],rs[r],x,mid+1,b);
    }
    
    //區間(l,r]中第k小的數(不存在返回-1) 
    //記得詢問時l,r為root 
    int kth(int l,int r,int k,int a,int b)
    {
        if(k>cnt[r]-cnt[l])
            return -1;
        if(a==b)
            return a;
        
        int lp=cnt[ls[r]]-cnt[ls[l]],mid=(a+b)>>1;
        if(lp>=k)
            return kth(ls[l],ls[r],k,a,mid);
        else
            return kth(rs[l],rs[r],k-lp,mid+1,b);
    }
};

int n,m;
int a[N];
ChairmanTree t;

int main()
{
    int T;
    scanf("%d",&T);
    while(T--)
    {
        t.init();
        
        scanf("%d%d",&n,&m);
        for(int i=1;i<=n;i++)
        {
            scanf("%d",&a[i]);
            t.insert(i,a[i],1,n);
        }
        
        int sz=n,ans=0;
        for(int i=1;i<=m;i++)
        {
            int op,x,y;
            scanf("%d%d",&op,&x);
            x^=ans;
            
            if(op==1)
            {
                if(a[x]<=n)
                {
                    t.insert(++sz,a[x],1,n);
                    a[x]+=n;
                }
            }
            else
            {
                scanf("%d",&y);
                y^=ans;
                
                int num=y-1?t.order(root[x],root[sz],y-1,1,n):0;
                
                ans=t.kth(root[x],root[sz],num+1,1,n);
                if(ans==-1)
                    ans=n+1;
                printf("%d\n",ans);
            }
        }
    }
    return 0;
}
View Code

 

(完)


免責聲明!

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



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