Training for 分块

分块 Study data Link:http://hzwer.com/8053.html    // hzwer讲的很好很全

树上莫队Study Link:http://codeforces.com/blog/entry/43230   ( ery nice  并且有题目推荐

莫队算法时间复杂度O( n * sqrt(n) )证明:

由于每一块的大小为sqrt(n),故有sqrt(n)块,按照所属块为first key ,右端点为second key,考虑每一块,因为每块的右端点是Increase,故右端点最大的移动为N ,左端点的最大移动为 k × sqrt(n),故总复杂度为O(n×sqrt(n) + m×sqrt(n))  ≈  O( n * sqrt(n) )


一道很牛逼的题 

题意

给一个n的数的序列,现有q次询问, 1代表是修改操作,2代表是查询操作,对于每次查询输出[1,1e6]以内出现偶数次最小的树(没有出现也算是偶数次)   (a[i],n,q<=1e6)

分析

对[1,1e6]分块,每次修改之前的数所在的块和修改后所在的块即可

代码:待补


CDOJ 1324

分析

简单的单点更新,区间最大值

时间复杂度( n×sqrt(n) )

#include<iostream>
#include<cstdio>
#include<cstring>
#include<cmath>
#include<vector>
#define ll long long
using namespace std;
const int maxn = 1e5+7;

//blcok:每块的大小
//num:块的数量
//l[]:每块的左边界,r[]:每块的右边界
//belong[]:每个位置的数属于那一块
int n,q;
int belong[maxn],block,l[maxn],r[maxn],num;
ll a[maxn],Max[maxn];

void build(){
    block=sqrt(n);
    num=n/block; if(n%num) num++;
    for(int i=1;i<=num;i++)
        l[i]=(i-1)*block, r[i]=i*block; // 处理出每块的左右边界
    r[num]=n;
    for(int i=1;i<=n;i++)
        belong[i]=(i-1)/block+1; // 每个位置的数属于那一块
    for(int i=1;i<=num;i++)
        for(int j=l[i];j<=r[i];j++)
          Max[i]=max(Max[i],a[j]);
}

void update(int x,int y){
    a[x]+=y;
    Max[belong[x]]=max(Max[belong[x]],a[x]);
}

ll ask(int x,int y)
{
    ll ans=0;
    if(belong[x]==belong[y]){
        for(int i=x;i<=y;i++)
            ans=max(ans,a[i]);
    }
    else {
        for(int i=x;i<=r[belong[x]];i++)
            ans=max(ans,a[i]);
        for(int i=belong[x]+1;i<=belong[y]-1;i++)
            ans=max(ans,Max[i]);
        for(int i=l[belong[y]];i<=y;i++)
            ans=max(ans,a[i]);
    }
    return ans;
}

int main()
{
    scanf("%d%d", &n, &q);
    build();
    for(int i=1;i<=q;i++)
    {
        int op,l,r;
        scanf("%d%d%d", &op, &l, &r);
        if(op==1) update(l,r);
        else
            printf("%lld\n", ask(l,r));
    }
    return 0;
}

莫队入门

codeforces 617E. XOR and Favorite Number

题意

给你一个n个数的序列,有m次询问,每次询问问你 L,R, K,区间[l,r]  ,有多少对(i,j)( l ≤ i ≤ j ≤ r )使得  the xor of the numbers ai, ai + 1, ..., aj is equal to k.

 (1 ≤ n, m ≤ 1e5, 0 ≤ k ≤ 1e6,0 ≤ ai ≤ 1e6) 

分析

由于xor具有 A^A=0性质,处理出xor前缀,区间[L,R]的xor值即为pre[R]^pre[L-1],直接上莫队,一边更新区间[L,R]每个异或值出现的次数,统计时每次只需要询问修改的区间更新(加/减)即可

 trick:需要注意更新的先后顺序以及处理[L,R]区间,由于xor的特殊性,需要处理L-1  !!!

#include<bits/stdc++.h>
#define ll long long
using namespace std;
const int maxn = 3e6 +7;

int belong[maxn],a[maxn],num,block, ans[maxn];
ll f[maxn];
int n,m,k;

struct node
{
    int l,r,id;
    friend bool operator < (node a,node b)
    {
        if(belong[a.l] == belong[b.l])
            return a.r<b.r;
        return belong[a.l] < belong[b.l];
    }
}q[maxn];


ll Ans=0;

void del(int p)   // 因为删除的这个位置不在考虑的区间内,所以先更新ans,在查询
{
    ans[a[p]]--;
    Ans-=ans[a[p]^k];
}

void add(int p)   // 因为加的这个数如果恰好为L-1,这个应不作为考虑,不是L-1的话,这个位置会在下次统计上,所以先查询后更新
{
    Ans+=ans[a[p]^k];
    ans[a[p]]++;
}

void solve()
{
    int L=1,R=0;
    for(int i=1;i<=m;i++)
    {
        while(L<q[i].l)  // 由于要得到A[l-1]^A[r] , 故应该先更新,再L++;
        {
            del(L-1);
            L++;
        }
        while(L>q[i].l){ //当L>q[i].l ,为了便于思考取L=q[i].l+1,其实已经处理了q[i].l这个位置,因为上次统计的时候利用了这个位置
            L--;
            add(L-1);
        }
        while(R<q[i].r){ // 右端点没有什么特殊的
            R++;
            add(R);
        }
        while(R>q[i].r){
            del(R);
            R--;
        }
        f[q[i].id]=Ans;
    }
}

int main()
{
    ans[0]=1;       // 注意什么都不取得时候也有一种情况,当A[t]^K=0时,也有一种情况,L=0时
    scanf("%d%d%d", &n, &m, &k);
    block=int(sqrt(n));
    for(int i=1;i<=n;i++){
        scanf("%d", &a[i]);
        a[i]^=a[i-1];
        belong[i]=(i-1)/block+1;
    }
    for(int i=1;i<=m;i++){
        scanf("%d%d", &q[i].l, &q[i].r);
        q[i].id=i;
    }
    sort(q+1,q+m+1);
    solve();
    for(int i=1;i<=m;i++)
        printf("%lld\n", f[i]);
    return 0;
}

bzoj2038 小z的袜子(hose)

分析

莫队板子题

#include<bits/stdc++.h>
#define ll long long
using namespace std;
const int maxn = 1e6+7;

int belong[maxn],a[maxn],num,block, ans[maxn];
ll fz[maxn],fm[maxn];
int n,m,k;

struct node{
    int l,r,id;
    friend bool operator < (node a,node b)
    {
        if(belong[a.l] == belong[b.l])
            return a.r<b.r;
        return belong[a.l] < belong[b.l];
    }
}q[maxn];

ll gcd(ll a, ll b)
{
    return b==0? a : gcd(b, a%b);
}


ll Ans=0;

void del(int p)
{
   Ans-=ans[a[p]]-1;
   ans[a[p]]--;
}

void add(int p)
{
    ans[a[p]]++;
    Ans+=ans[a[p]]-1;
}

void solve()
{
   int L=1,R=0;
   for(int i=1;i<=m;i++)
   {
       while(L<q[i].l)
        {
           del(L);
           L++;
       }
       while(L>q[i].l)
       {
           L--;
           add(L);
       }
       while(R<q[i].r)
       {
           R++;
           add(R);
       }
       while(R>q[i].r)
       {
           del(R);
           R--;
       }
       ll k=((1LL)*(q[i].r-q[i].l+1))*(q[i].r-q[i].l)/2;
       if(Ans!=0)
       {
           ll g=gcd(Ans,k);
           fz[q[i].id]=Ans/g;
           fm[q[i].id]=k/g;
       }
       else
       {
           fz[q[i].id]=Ans;
           fm[q[i].id]=1;
       }
   }
}

int main()
{
    scanf("%d%d", &n, &m, &k);
    block=int(sqrt(n));
    for(int i=1;i<=n;i++){
        scanf("%d", &a[i]);
        belong[i]=(i-1)/block+1;
    }
    for(int i=1;i<=m;i++){
        scanf("%d%d", &q[i].l, &q[i].r);
        q[i].id=i;
    }
    sort(q+1,q+m+1);
    solve();
    for(int i=1;i<=m;i++)
        printf("%lld/%lld\n", fz[i],fm[i]);
    return 0;
}

codeforces 620 F. Xors on Segments

题意

给一个n个数的序列,给出m个询问 l, r, 问从区间[L,R]中选两个数 a,b(a<=b,可以同一个数),f=a^(a+1)....^(b-1)^b 的最大值(1 ≤ n ≤ 5e4,  1 ≤ m ≤ 5e3 ,1 ≤ ai ≤ 1e6,1<=L<=R<=n

分析

数据水了放过了n^2,但数组开太大也会T,正解:莫队+Tire

solution:暴力的复杂度是(m×n^2)肯定gg,那么考虑优化,对所有的询问离线,dp[i]: 以a[i]作为开头的元素的最大值,询问所有 j > i,当以 j 为右端点的询问的左端点<= i时更新

时间复杂度:O(n^2 + n×m)

#include<bits/stdc++.h>
#define ll long long
using namespace std;
const int maxn = 5e4+7;

int a[maxn],pre[1000007],n,m;
vector<pair<int,int> >v[maxn];
int ans[maxn];

int main()
{
    for(int i=1;i<1000007;i++)
        pre[i]=pre[i-1]^i;
    scanf("%d%d",&n, &m);
    for(int i=1;i<=n;i++)
    {
        scanf("%d",&a[i]);
    }
    int l,r;
    for(int i=1;i<=m;i++)
    {
        scanf("%d%d", &l, &r);
        v[r].push_back({l,i});
    }
    int dp;
    for(int i=1;i<=n;i++)
    {
        dp=0;
        for(int j=i;j<=n;j++)
        {
            dp=max(dp, pre[a[j]] ^ pre[a[i]] ^ min(a[i],a[j]));
            for(auto it : v[j])
            {
                if(it.first<=i)  ans[it.second]=max(ans[it.second],dp);
            }
        }
    }
    for(int i=1;i<=m;i++)
    {
        printf("%d\n",ans[i]);
    }
    return 0;
}

BZOJ 1086 糖果公园   DFS+贪心

分析

dfs的时候统计子树叶子数量,大于等于b时归为一个省,省会就为当前父节点,怎么把子树节点保存下来呢?用一个栈即可。最后剩下小于b直接给最后一个省即可

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


const int maxn = 1000+2;

int belong[maxn],q[maxn],top,sz[maxn],cap[maxn],sum;
int head[maxn],nxt[maxn*2],to[maxn*2],tot,k;
int n,b;

void addedge(int u,int v)
{
    to[++tot]=v;
    nxt[tot]=head[u];
    head[u]=tot;
}

void dfs(int x,int fa)
{
    q[++top]=x;
    for(int i=head[x];i;i=nxt[i])
    {
        int v=to[i];
        if(v!=fa)
        {
            dfs(v,x);
            if(sz[x]+sz[v]>=b)
            {
                sz[x]=0;
                cap[++sum]=x;
                //cout<<v<<' '<<x<<' '<<sum<<endl;
                while(q[top]!=x)
                {
                    belong[q[top]]=sum;
                    --top;
                }
            }
            else
                sz[x]+=sz[v];
        }
    }
    sz[x]++;
}

int main()
{
    scanf("%d%d", &n, &b);
    if(n<b){puts("0");return 0;}
    int u,v;
    for(int i=1;i<=n-1;i++){
        scanf("%d%d", &u, &v);
        addedge(u,v);
        addedge(v,u);
    }
    dfs(1,0);
    if(n==1)
        cout<<1<<endl<<1<<endl<<1<<endl;
    else
    {
        printf("%d\n",sum);
        for(int i=1;i<=n;i++)
        {
            if(!belong[i])
                belong[i]=sum;
        }
        for(int i=1;i<=n;i++)
            printf("%d%c", belong[i], i==n ? '\n':' ');
        for(int i=1;i<=sum;i++)
            printf("%d%c", cap[i], i==n?'\n':' ');
    }
    return 0;
}

Codeforces 369E

题意

在x轴上,给n个区间 [ Li, Ri ],m个询问,每个询问包含k个点,对于每个询问输出这些点在几个区间里 (n,m<=3e5, L,R<= 1e6,所有询问 k 的和不超过 3e5 ) 

分析

正着做,无论是对于每个点考虑在那些区间 和 那些区间包括那些点都没有想到合适复杂度,换个角度,考虑点的补集所组成的线段集合完全包含那些线段,那么这些线段对答案一定没有贡献,

故那么我们怎么check每个询问点的补集所组成的线段和所给线段的关系?考虑对所有询问离线,按照线段左端点为first key(从大到小), 右端点为second key(从小到大),每个线段的询问时间为third

key(从小到大,因为当已经线段和询问线段重合的情况),这样可以保证每次询问的左端点是递减的,考虑用树状数组加速这个过程,每次更新右端点即可,依次更新即可

#include<bits/stdc++.h>
#define ll long long
using namespace std;
const int maxn = 1e6+7;


int c[maxn];
int ans[maxn];

struct node{
    int l,r,id;
    friend bool operator < (node a,node b)
    {
        if(a.l != b.l)
            return a.l>b.l;
        else if(a.r!=b.r)
            return a.r < b.r;
        else
            return a.id<b.id;
    }
}t[maxn];

int lowbit(int x){
    return x&(-x);
}
int sum(int  x){
    int sum=0;
    while(x>0){
        sum+=c[x];
        x-=lowbit(x);
    }
    return sum;
}
int modfiy(int x,int val){
    while(x<=1000000){
        c[x]+=val;
        x+=lowbit(x);
    }
}
int n,q;
int main()
{
    scanf("%d%d",&n,&q);
    for(int i=1;i<=n;i++)
    {
        scanf("%d%d", &t[i].l,&t[i].r);
        t[i].id=0;
    }
    int k;
    int tot=n;
    for(int i=1;i<=q;i++)
    {
        scanf("%d", &k);
        int st=0;
        for(int j=1;j<=k;j++)
        {
            int x;
            scanf("%d", &x);
            if(!st)
            {
                if(x>1)
                {
                    t[++tot].l=1;
                    t[tot].r=x-1;
                    t[tot].id=i;
                }
                st=x;
            }
            else
            {
                if(x-1<st+1)
                   {

                   }
                else
                {
                    t[++tot].l=st+1;
                    t[tot].r=x-1;
                    t[tot].id=i;
                }
                st=x;
            }

        }
       if(st+1<=1000000)
       {
          t[++tot].l=st+1;
          t[tot].r=1000000;
          t[tot].id=i;
       }
    }
    sort(t+1, t+tot+1);
    for(int i=1;i<=tot;i++)
    {
        if(t[i].id==0)
        {
            modfiy(t[i].r,1);
        }
        else
        {
            ans[t[i].id]+=sum(t[i].r);
        }
    }
    for(int i=1;i<=q;i++)
        printf("%d\n", n-ans[i]);
    return 0;
}

Codeforces 375D   树上莫队

题意

给一颗n个节点带权树(权值=颜色),现有m个询问,每个询问问u,k,问以u为根的子树,出现的所有颜色中次数>=k次的数量(根为1,n,m<=1e5 )

分析

Study Linkdsu on tree

解法:莫队/dsu on tree+bit

莫队:求出dfs序,可知每个点的子树的范围,转化为区间上的序列的莫队,每次询问一个根节点相当于询问它的子树区间,对区间分块排序后直接上莫队即可 ( 类比区间

          trick:处理>=k的数量很巧妙啊,因为每个颜色数量是逐渐变大的,故直接用一个数组递推过去即可,O(1)

#include<bits/stdc++.h>
#define ll long long
using namespace std;
const int maxn = 1e5+7;

int n,m,col[maxn],rcol[maxn],sum[maxn],cnt[maxn];
int head[maxn*2],nxt[maxn*2],to[maxn*2],tot;
int L[maxn],R[maxn], Ans[maxn], block, belong[maxn];

void addedge(int u,int v)
{
    to[++tot]=v;
    nxt[tot]=head[u];
    head[u]=tot;
}

struct node{
    int l,r,id,k;
    friend bool operator <(node a,node b)
    {
        if(a.l/block!=b.l/block)
            return a.l/block < b.l/block;
        return a.r<b.r;
    }
}q[maxn];

int tms;
void dfs(int u,int fa){
    L[u]=++tms;
    for(int i=head[u];i;i=nxt[i]){
        int v=to[i];
        if(v!=fa)
            dfs(v,u);
    }
    R[u]=tms;
}

int main()
{
    scanf("%d%d", &n, &m);
    for(int i=1;i<=n;i++)
       scanf("%d", &col[i]);
    block=int(sqrt(n));
    int u,v;
    for(int i=1;i<=n-1;i++){
        scanf("%d%d", &u, &v);
        addedge(u,v);
        addedge(v,u);
    }
    dfs(1,0);

    int x,kk;
    for(int i=1;i<=m;i++){
        scanf("%d%d", &x, &kk);
        q[i].l=L[x];
        q[i].r=R[x];
        q[i].k=kk;
        q[i].id=i;
    }
    sort(q+1,q+m+1);
    for(int i=1;i<=n;i++)
        rcol[i]=col[i];
    for(int i=1;i<=n;i++)
        col[L[i]]=rcol[i];
    int L=1,R=0;
    for(int i=1;i<=m;i++)
    {
        while(L<q[i].l)
        {
            sum[cnt[col[L]]]--;
            cnt[col[L]]--;
            L++;
        }
        while(L>q[i].l)
        {
            L--;
            cnt[col[L]]++;
            sum[cnt[col[L]]]++;
        }
        while(R<q[i].r)
        {
            R++;
            cnt[col[R]]++;
            sum[cnt[col[R]]]++;
        }
        while(R>q[i].r)
        {
            sum[cnt[col[R]]]--;
            cnt[col[R]]--;
            R--;
        }
        Ans[q[i].id]=sum[q[i].k];
    }
    for(int i=1;i<=m;i++)
        printf("%d\n", Ans[i]);
    return 0;
} 

WC2013 糖果公园(UOJ 58)

分析

留坑,树上带修改莫队 ,vfk题解Link


树上莫队  SPOJ  COT2 - Count on a tree II / BZOJ 3757

题意

SPOJ:给一颗点权数,多次询问路径(a,b)上有多少个权值不同的点(n<=4e5,m<=1e6)

BZOJ:

分析

直观的做法:每次从lca(a,b)分别走到a,b即可,但首先从lca(a,b)出发不能保证走的一定是a,b的路径,最坏的情况是走整棵树,gg

莫队:考虑深搜将树分块,将树上的序列转化成区间上的序列,按照区间上的莫队方法来做

转移:考虑从(u,v)->(u',v')

设S(u,v):u到v路径上的点集,则S(u,v)=S(root,u) xor S(root,v) xor lca(u,v) ( xor运算=对称差:简单的说就是去掉出现两次的 )

由于lca(u,v)很麻烦,那么我们设T(u,v)=S(root,u) xor S(root,v),则T(u',v)=S(root,u') xor S(root,v) (现考虑一个点移动)

T(u',v) xor T(u,v) = S(root,u') xor S(root,v)xor S(root,u) xor S(root,v)

T(u',v) xor T(u,v) = S(root,u') xor S(root,u) = T(u',u)

两边同时 xor T(u,v) 

T(u',v) = T(u,v) xor T(u',u)

=> S(a',b) = S(a,b) xor S(a,a') xor lca(a',b)

=> S(a',b')=S(a,b) xor S(a,a') xor S(b,b') xor lca(a',b')

所以:从(a,b)->(a',b') 只需要 a-> a' 和 b-> b'路径上的点取反 ( 除lca(u',v) )的情况取反即可 

转移的方法:通过预处理的深度,不断调节深度,直至到达两点lca,但没有处理lca

trick:注意lca(u',v')不是取反,直接取相反的情况即可 ( 即没有对lca(u',v')的数量修改),因为在转移的时候并没有将lca(u',v')考虑进去,

           那么有一个问题,若lca(u,v)==lca(u',v'),这样处理没问题,但lca(u,v) != lca(u',v'),其实通过画图观察不难发现,u->u'和v->v' 转移过程都没有改变lca(u,v),故不影响

           所以我们只要大胆的把u->u'和v->v' 转移过去就好,最后将lca(u',v')的贡献加进去即可

总结:1. 深搜 分块+预处理深度           2. 求lca主       3. sort     4.莫队转移得到answer

!!!!Trick:树上莫队按块排序 L, R的所在的块都要排序,因为在序列上(eg:数组)从小到大直接就是从近到远,但树上的序号却没有任何关系,因此必须要把L,R的块都排序

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

const int maxn = 4e5+7;

int head[maxn],nxt[maxn*2],to[maxn*2],tot;
int col[maxn],n,m,u,v,ans[maxn],Ans;
int belong[maxn],block;
int fa[maxn][32],d[maxn],f[maxn];
bool vis[maxn],vv[maxn];
int g[maxn];
unordered_map<int,int>rg;

struct node{
    int l,r,id;
    friend bool operator < (node a,node b){
        if(belong[a.l] != belong[b.l])  return belong[a.l] < belong[b.l];
        else return belong[a.r] < belong[b.r];
    }
}q[maxn];


void addedge(int u,int v)
{
    to[++tot]=v;
    nxt[tot]=head[u];
    head[u]=tot;
}

int s[maxn],cnt,sz[maxn],num;
void dfs(int x)
{
    vis[x]=1;
    s[++cnt]=x;
    for(int i=head[x];i;i=nxt[i])
    {
        int v=to[i];
        if(!vis[v])
        {
            d[v]=d[x]+1;
            fa[v][0]=x;
            dfs(v);
            if(sz[x] + sz[v] >= block)
            {
                ++num;
                sz[x]=0;
                while(s[cnt]!=x)
                {
                    belong[s[cnt]]=num;
                    vv[s[cnt]]=1;
                    --cnt;
                }
            }
            else
                sz[x]+=sz[v];
        }
    }
    sz[x]++;
}

void init_rmq()
{
   // fa[1][0]=1;
    for(int j=1;j<=30;j++)
        for(int i=1;i<=n;i++)
        fa[i][j]=fa[fa[i][j-1]][j-1];
}

int LCA(int u,int v)
{
    if(d[u]<d[v])
        swap(u,v);
    int dc=d[u]-d[v];
    for(int i=0;i<30;i++){ //�����度
        if((1<<i)&dc)
            u=fa[u][i];
    }
    if(u==v) return u;      // �个��好���个��lca
    for(int i=30;i>=0;i--){ //���以跳��大�步��使�(u,v)�lca��
        if(fa[u][i] != fa[v][i])
        {
            u=fa[u][i];
            v=fa[v][i];
        }
    }
    return  fa[u][0]; //u�v�����lca���
}

void rev(int x)
{
    if(!vis[x]){
        f[col[x]]++;
        if(f[col[x]]==1)
            Ans++;
        vis[x]=1;
    }
    else{
        f[col[x]]--;
        if(f[col[x]]==0)
            Ans--;
        vis[x]=0;
    }
}

void solve(int u,int v)
{
    while(u!=v)
    {
        if(d[u]<d[v]){
            rev(v),v=fa[v][0];
        }
        else
        {
            rev(u),u=fa[u][0];
        }
    }
}

int main()
{
    scanf("%d%d", &n, &m);
    block=int(sqrt(n));
    for(int i=1;i<=n;i++)
    {
        scanf("%d", &col[i]);
        g[i]=col[i];
    }
    sort(g+1,g+n+1);
    int point=unique(g+1,g+n+1)-g-1;
    for(int i=1;i<=point;i++)
        rg[g[i]]=i;
    for(int i=1;i<=n;i++)
      col[i]=rg[col[i]];
    for(int i=1;i<=n-1;i++)
    {
        scanf("%d%d", &u, &v);
        addedge(u,v),addedge(v,u);
    }
  //  d[1]=1;
    dfs(1);
    ++num;
    for(int i=1;i<=n;i++){
        if(!vv[i])
            belong[i]=num;
    }
    init_rmq();
    memset(vis,0,sizeof(vis));
    for(int i=1;i<=m;i++){
        scanf("%d%d", &u, &v);
        if(belong[u] > belong[v])
            swap(u,v);
        q[i].l=u;
        q[i].r=v;
        q[i].id=i;
    }
    sort(q+1,q+m+1);
    int lca=LCA(q[1].l,q[1].r);
    solve(q[1].l,q[1].r);
    ans[q[1].id]=Ans+!(f[col[lca]]);
    for(int i=2;i<=m;i++)
    {
        lca=LCA(q[i].l,q[i].r);
        solve(q[i-1].l,q[i].l);
        solve(q[i-1].r,q[i].r);
        ans[q[i].id]=Ans+!(f[col[lca]]);
    }
    for(int i=1;i<=m;i++)
        printf("%d\n",ans[i]);
    return 0;
}

猜你喜欢

转载自www.cnblogs.com/Deadline/p/9163421.html