线段树解题技巧

前言

线段树是一种在 \(\log\) 时间内维护区间信息的数据结构,其维护的信息具有区间可加性。

区间可加性,也就是由区间 \(A\) 和区间 \(B\),可以推出 \(A\cup B\)

上面说到的区间,指的是区间内维护的信息。

如区间和,区间平方和,区间最值,区间最大子段,区间最长连续子段,这类问题就是具有区间可加性的。

关于线段树维护的题目,分为两类,一类是好维护的,一类是不好维护的,体现在修改与查询的关系并不大。下面分这两类进行分析。

好维护

好维护的信息通常是由修改可以推出查询,比如修改是将一个区间加上某个数,查询是查区间和,这时可以直接由修改推出查询。

P3373 【模板】线段树 2

比单纯的区间加稍微复杂一点。

这题显然是好维护的,对于一个区间加上一个数,很典,乘上一个数,考虑添加一个乘法懒标记。记 \(tag1\) 为加法标记,\(tag2\) 为乘法标记。

这时我们要考虑,加法标记和乘法标记的优先级。对于一个运算:

\[((x+1)\times 4+6)\times 7 \]

不难发现,\(1\) 乘了 \(4\times 7\),但是 \(6\) 只乘了 \(7\),这提示我们不能直接将 \(tag1\) 累加至 \(sum\),再用 \(tag2\) 去乘,此时我们将这个柿子的顺序变换一下:

\[x\times 4\times 7+1\times 4\times 7+6\times 7 \]

这启示我们加法标记 \(tag1\) 存的实际是 \(1\times 4\times 7+6\times 7\)\(tag2\) 存的是 \(4\times 7\),在最后计算 \(sum\) 时,采取先乘后加的方法。对于维护 \(tag\) 也是类似。

点击查看代码
#include<bits/stdc++.h>
using namespace std;

typedef long long LL;
typedef unsigned long long ULL;
LL read() {
    LL sum=0,flag=1; char c=getchar();
    while(c<'0'||c>'9') {if(c=='-') flag=-1; c=getchar();}
    while(c>='0'&&c<='9') {sum=sum*10+c-'0'; c=getchar();}
    return sum*flag;
}

const int N=1e5+10;
int n,q,m;
LL tr[N<<2],tag1[N<<2],tag2[N<<2];

void add(int nd,int l,int r,LL x1,LL x2) {
    tag1[nd]=(tag1[nd]*x2+x1)%m;
    tag2[nd]=(tag2[nd]*x2)%m;
    tr[nd]=(tr[nd]*x2%m+(r-l+1)*x1%m)%m;
}

void pushdown(int nd,int l,int r) {
    int mid=l+r>>1;
    add(nd<<1,l,mid,tag1[nd],tag2[nd]);
    add(nd<<1|1,mid+1,r,tag1[nd],tag2[nd]);
    tag1[nd]=0; tag2[nd]=1;
}

void pushup(int nd) {
    tr[nd]=(tr[nd<<1]+tr[nd<<1|1])%m;
}

void change(int nd,int l,int r,int x,int y,LL x1,LL x2) {
    if(r<x||l>y) return ;
    if(l>=x&&r<=y) return add(nd,l,r,x1,x2);
    pushdown(nd,l,r);
    int mid=l+r>>1;
    change(nd<<1,l,mid,x,y,x1,x2);
    change(nd<<1|1,mid+1,r,x,y,x1,x2);
    pushup(nd);
}

LL ask(int nd,int l,int r,int x,int y) {
    if(r<x||l>y) return 0;
    if(l>=x&&r<=y) return tr[nd];
    pushdown(nd,l,r);
    int mid=l+r>>1;
    return (ask(nd<<1,l,mid,x,y)+ask(nd<<1|1,mid+1,r,x,y))%m;
}

int main() {
    // freopen("a.in","r",stdin);
    // freopen("a.out","w",stdout);

    n=read(); q=read(); m=read();
    for(int i=1;i<=n*4;i++) {
        tag1[i]=0;
        tag2[i]=1;
    }
    for(int i=1;i<=n;i++) {
        LL x=read();
        change(1,1,n,i,i,x,1);
    }
    while(q--) {
        int opt=read(),x=read(),y=read();
        LL k;
        if(opt==1) {
            k=read();
            change(1,1,n,x,y,0,k);
        }
        else if(opt==2) {
            k=read();
            change(1,1,n,x,y,k,1);
        }
        else {
            cout<<ask(1,1,n,x,y)<<'\n';
        }
    }

    return 0;
}

P1471 方差

对于平均数,这是很好维护的,只需维护区间和即可。

对于方差,我们利用高中数学知识将其化成如下形式:

\[s^2=\frac{1}{n} \sum_{i=1}^{n} (A_i-\overline{A})^2=\frac{1}{n} \sum_{i=1}^{n} A_i^2 -\overline{A}^2 \]

对于这个玩意,维护区间平方和即可,考虑修改对查询的影响,若给\(a_{l\sim r}+k\),那么区间平方和为 \((a_{l}+k)^2+...+(a_{r}+k)^2\)\(a_{l}^2+...+a_{r}^2+2k(a_l+...+a_r)+(r-l+1)\times k^2\).

对于上面的式子,显然是好维护的,维护区间平方和,区间和,即可实现更新。

点击查看代码
#include<bits/stdc++.h>
using namespace std;

typedef long long LL;
typedef unsigned long long ULL;
LL read() {
    LL sum=0,flag=1; char c=getchar();
    while(c<'0'||c>'9') {if(c=='-') flag=-1; c=getchar();}
    while(c>='0'&&c<='9') {sum=sum*10+c-'0'; c=getchar();}
    return sum*flag;
}

const int N=1e5+10;
int n,m;
double sum1[N<<2],sum2[N<<2],tag[N<<2];
struct node {
    double s1,s2;
};

void add(int nd,int l,int r,double k) {
    tag[nd]+=k;
    sum2[nd]=sum2[nd]+2*k*sum1[nd]+(double)(r-l+1)*k*k;
    sum1[nd]+=(r-l+1)*k;
}

void pushdown(int nd,int l,int r) {
    int mid=l+r>>1;
    if(!tag[nd]) return ;
    add(nd<<1,l,mid,tag[nd]);
    add(nd<<1|1,mid+1,r,tag[nd]);
    tag[nd]=0;
}

void pushup(int nd) {
    sum1[nd]=sum1[nd<<1]+sum1[nd<<1|1];
    sum2[nd]=sum2[nd<<1]+sum2[nd<<1|1];
}

void change(int nd,int l,int r,int x,int y,double k) {
    if(r<x||l>y) return ;
    if(l>=x&&r<=y) return add(nd,l,r,k);
    int mid=l+r>>1;
    pushdown(nd,l,r);
    change(nd<<1,l,mid,x,y,k);
    change(nd<<1|1,mid+1,r,x,y,k);
    pushup(nd);
}

node query(int nd,int l,int r,int x,int y) {
    if(r<x||l>y) return {0,0};
    if(l>=x&&r<=y) return {sum1[nd],sum2[nd]};
    pushdown(nd,l,r);
    int mid=l+r>>1;
    node x1=query(nd<<1,l,mid,x,y);
    node x2=query(nd<<1|1,mid+1,r,x,y);
    return {x1.s1+x2.s1,x1.s2+x2.s2};
}

int main() {
    // freopen("a.in","r",stdin);
    // freopen("a.out","w",stdout);

    n=read(); m=read();
    for(int i=1;i<=n;i++) {
        double x; cin>>x;
        change(1,1,n,i,i,x);
    }
    while(m--) {
        int opt=read(),x=read(),y=read();
        double k;
        if(opt==1) {
            cin>>k;
            change(1,1,n,x,y,k);
        }
        else {
            node ans=query(1,1,n,x,y);
            if(opt==2) {
                printf("%.4lf\n",ans.s1*1.0/(y*1.0-x*1.0+1.0));
            }
            else {
                double avg=ans.s1*1.0/((y-x+1)*1.0);
                double kkk=ans.s2*1.0/((y-x+1)*1.0);
                printf("%.4lf\n",kkk-avg*avg);
            }
        }
    }

    return 0;
}

P4513 小白逛公园

维护最大子段和的板子题。

考虑将两个区间拼在一起如何更新答案。

可以考虑维护一个区间左边连续的最大值,右边连续的最大值。那么将新区间的最大子段和可以由左右区间的答案构成,也可以有左区间的右边最大值,加上右区间的左边最大值加起来,取最大值即可。

左右的连续最大值是好求的,具体看代码。

点击查看代码
#include<bits/stdc++.h>
using namespace std;

typedef long long LL;
typedef unsigned long long ULL;
LL read() {
    LL sum=0,flag=1; char c=getchar();
    while(c<'0'||c>'9') {if(c=='-') flag=-1; c=getchar();}
    while(c>='0'&&c<='9') {sum=sum*10+c-'0'; c=getchar();}
    return sum*flag;
}

const int N=5e5+10;
int n,m;
struct node {
    int sum,maxn,lmax,rmax;
}tr[N<<2];

node merge(node x,node y) {
    node k;
    k.sum=x.sum+y.sum;
    k.lmax=max(x.lmax,x.sum+y.lmax);
    k.rmax=max(y.rmax,x.rmax+y.sum);
    k.maxn=max(max(x.maxn,y.maxn),x.rmax+y.lmax);
    return k;
}

void change(int nd,int l,int r,int p,int k) {
    if(r<p||l>p) return ;
    if(l==r&&l==p) {
        tr[nd].sum=tr[nd].lmax=tr[nd].rmax=tr[nd].maxn=k;
        return ;
    }
    int mid=l+r>>1;
    change(nd<<1,l,mid,p,k);
    change(nd<<1|1,mid+1,r,p,k);
    tr[nd]=merge(tr[nd<<1],tr[nd<<1|1]);
}

node query(int nd,int l,int r,int x,int y) {
    if(l>=x&&r<=y) return tr[nd];
    int mid=l+r>>1;
    node s; int flag=0;
    if(mid>=x) {
        node k=query(nd<<1,l,mid,x,y);
        s=k;flag=1;
    }
    if(mid+1<=y) {
        node k=query(nd<<1|1,mid+1,r,x,y);
        if(!flag) s=k;
        else s=merge(s,k);
    }
    return s;
}

int main() {
    // freopen("a.in","r",stdin);
    // freopen("a.out","w",stdout);

    n=read(); m=read();
    for(int i=1;i<=n;i++) {
        int x=read();
        change(1,1,n,i,x);
    }
    while(m--) {
        int k=read(),a=read(),b=read();
        if(k==1) {
            if(a>b) swap(a,b);
            node x=query(1,1,n,a,b);
            cout<<x.maxn<<'\n';
        }
        else {
            change(1,1,n,a,b);
        }
    }


    return 0;
}

类似问题:[SHOI2015] 脑洞治疗仪,但是这题求的是最长连续 \(0\) 的个数,和上文的最大子段和略有不同,注意区分。

「Wdsr-2.7」文文的摄影布置

比较有意思的线段树。

注意到要维护的式子是 \(A_i+A_k-min(B_j)(i<j<k)\)\(\max\),考虑将式子拆成两个部分:\(A_i\)\(A_k-min(B_j)\) 或者 \(A_k\)\(A_i-min(B_j)\) ,之所以将式子拆成两个部分是因为 \(i,j\)\(k,j\) 的相对顺序不一样。

维护是简单的,只需维护区间 \(A_{\max}\),区间 \(B_{\min}\),区间 \(A_i-min(B_j)\),区间 \(A_k-min(B_j)\),区间 \(ans\) 即可。

点击查看代码
#include<bits/stdc++.h>
using namespace std;

typedef long long LL;
typedef unsigned long long ULL;
LL read() {
    LL sum=0,flag=1; char c=getchar();
    while(c<'0'||c>'9') {if(c=='-') flag=-1; c=getchar();}
    while(c>='0'&&c<='9') {sum=sum*10+c-'0'; c=getchar();}
    return sum*flag;
}

const int N=5e5+10;
const int INF=1e9;
int n,m;
int a[N],b[N];
struct node {
    int ans,cij,ckj,maxa,minb;
}tr[N<<2];

node merge(node x,node y) {
    node k;
    k.maxa=max(x.maxa,y.maxa);
    k.minb=min(x.minb,y.minb);
    k.cij=max(max(x.cij,y.cij),x.maxa-y.minb);
    k.ckj=max(max(x.ckj,y.ckj),y.maxa-x.minb);
    k.ans=max(max(x.ans,y.ans),max(x.maxa+y.ckj,x.cij+y.maxa));
    return k;
}

void change(int nd,int l,int r,int x,int a,int b) {
    if(r<x||l>x) return ;
    if(l==r) {
        tr[nd].maxa=a; tr[nd].minb=b;
        tr[nd].ans=tr[nd].cij=tr[nd].ckj=-INF;
        return ;
    }
    int mid=l+r>>1;
    change(nd<<1,l,mid,x,a,b);
    change(nd<<1|1,mid+1,r,x,a,b);
    tr[nd]=merge(tr[nd<<1],tr[nd<<1|1]);
}

node query(int nd,int l,int r,int x,int y) {
    if(l>=x&&r<=y) return tr[nd];
    int mid=l+r>>1;
    node s; int flag=0;
    if(mid>=x) {
        node k=query(nd<<1,l,mid,x,y);
        s=k; flag=1;
    }
    if(mid+1<=y) {
        node k=query(nd<<1|1,mid+1,r,x,y);
        if(flag) s=merge(s,k);
        else s=k;
    }
    return s;
}

int main() {
    n=read(); m=read();
    for(int i=1;i<=n;i++) a[i]=read();
    for(int i=1;i<=n;i++) b[i]=read();
    for(int i=1;i<=n;i++) {
        change(1,1,n,i,a[i],b[i]);
    }
    while(m--) {
        int opt=read(),x=read(),y=read();
        if(opt==1) {
            a[x]=y;
            change(1,1,n,x,a[x],b[x]);
        }
        else if(opt==2) {
            b[x]=y;
            change(1,1,n,x,a[x],b[x]);
        }
        else {
            cout<<query(1,1,n,x,y).ans<<'\n';
        }
    }

    return 0;
}

下面是非常规的题目,在线段树上维护差分数组。

区间最大公约数

口胡,没写代码。

如果直接维护每个区间的 \(\gcd\),那么在修改时无法实时更新区间的 \(\gcd\),毕竟区间 \(+k\)\(\gcd\) 显然不是 \(+k\)

这是就要提到 \(\gcd\) 的一个性质:

\[(a_1,a_2,...,a_n)=(a_1,a_2-a_1,...,a_n-a_{n-1}) \]

通过这个式子,我们发现,区间的 \(\gcd\) 与其差分序列的 \(\gcd\) 是相等的,所以我们考虑直接维护差分序列,这样对于区间加操作,转换为修改两个点的权值,在回溯时暴力更新 \(\gcd\) 即可,时间复杂度 \(O(\log n)\)

对于 \((l,r)\) 的询问,也就是 \((a[l],(b[l+1]...b[r]))\),其中 \(b\) 为差分数组,只需对于 \(a\) 再开一棵线段树即可。

总时间复杂度 \(O(m\log n)\)

不好维护

这类题通常不好维护,特征是询问很正常,但是修改却很奇怪,这类题目的通解便是——暴力修改,但同时修改会有性质,即一个点被修改的次数有限。

P4145 上帝造题的七分钟 2 / 花神游历各国

相当经典的题目,询问区间和,修改为开方。

对于开方,显然没有太好的处理办法,毕竟原来的区间和是 \(sum\),将区间内每一个数字开方后,区间和不是 \(\sqrt{sum}\),所以根本没法用懒标记维护。

但是我们考虑一个点最多会被开方几次,极限情况是 \(10^{12}\),被开方 \(6\) 次就到 \(1\),到 \(1\) 以后在开方值也不会变,利用这个性质,可以在每个节点记录该区间内是否全部数字都是 \(1\),如果是,就不用修改;否则一个一个暴力修改。

时间复杂度\(O(6m\log n)\)

点击查看代码
#include<bits/stdc++.h>
using namespace std;

typedef long long LL;
typedef unsigned long long ULL;
LL read() {
    LL sum=0,flag=1; char c=getchar();
    while(c<'0'||c>'9') {if(c=='-') flag=-1; c=getchar();}
    while(c>='0'&&c<='9') {sum=sum*10+c-'0'; c=getchar();}
    return sum*flag;
}

const int N=1e5+10;
int n,m;
LL a[N];
struct node {
    LL val;
    int cnt;
}tr[N<<2];

void build(int nd,int l,int r) {
    if(l==r) {
        tr[nd].val=a[l];
        if(a[l]==1) tr[nd].cnt=1;
        return ;
    }
    int mid=l+r>>1;
    build(nd<<1,l,mid);
    build(nd<<1|1,mid+1,r);
    tr[nd].cnt=tr[nd<<1].cnt+tr[nd<<1|1].cnt;
    tr[nd].val=tr[nd<<1].val+tr[nd<<1|1].val;
}

void change(int nd,int l,int r,int x,int y) {
    if(r<x||l>y) return ;
    if(l==r) {
        tr[nd].val=sqrt(tr[nd].val);
        if(tr[nd].val==1) tr[nd].cnt=1;
        return ;
    }
    int mid=l+r>>1;
    if(tr[nd<<1].cnt!=mid-l+1) change(nd<<1,l,mid,x,y);
    if(tr[nd<<1|1].cnt!=r-mid) change(nd<<1|1,mid+1,r,x,y);
    tr[nd].cnt=tr[nd<<1].cnt+tr[nd<<1|1].cnt;
    tr[nd].val=tr[nd<<1].val+tr[nd<<1|1].val;
}

LL query(int nd,int l,int r,int x,int y) {
    if(r<x||l>y) return 0;
    if(l>=x&&r<=y) return tr[nd].val;
    int mid=l+r>>1;
    return query(nd<<1,l,mid,x,y)+query(nd<<1|1,mid+1,r,x,y);
}

int main() {
    n=read();
    for(int i=1;i<=n;i++) a[i]=read();
    build(1,1,n);
    m=read();
    while(m--) {
        int k=read(),l=read(),r=read();
        if(l>r) swap(l,r);
        if(!k) {
            change(1,1,n,l,r);
        }
        else {
            cout<<query(1,1,n,l,r)<<'\n';
        }
    }

    return 0;
}

P7492 [传智杯 #3 决赛] 序列

注意:负数按照 32 位补码取按位或。

这句话是让我们用 \(int\) 去按位或,若用\(LL\),达不到补码的要求。

按位或有一个很好的性质:只会增大,不会减小。所以只要分析一个数最多被修改几次即可。

对于一个有效的修改,至少会将 \(a\) 的一位从 \(0\) 变成 \(1\),而\(a\) 至多 \(30\) 位,所以最多会处理 \(30\) 次。

对于一个区间,如何判读修改的 \(k\) 是不是有效修改呢,若改区间内所有数字都包含 \(k\) 的二进制位,显然是无效的,所以维护区间所有数字的按位并,记作 \(x\),若 \(x\;\and\;k=k\),即可说明所有数字都包含 \(k\),即为无效修改。对于有效修改,暴力修改即可。

时间复杂度 \(O(30m\log n)\)

注意,本题的子段和可以不选。

点击查看代码
#include<bits/stdc++.h>
using namespace std;

typedef long long LL;
typedef unsigned long long ULL;
LL read() {
    LL sum=0,flag=1; char c=getchar();
    while(c<'0'||c>'9') {if(c=='-') flag=-1; c=getchar();}
    while(c>='0'&&c<='9') {sum=sum*10+c-'0'; c=getchar();}
    return sum*flag;
}

const int N=1e5+10;
int n,m;
int a[N];
struct node {
    LL maxn,lmax,rmax,sum;
    int val;
}tr[N<<2];

node merge(node x,node y) {
    node k;
    k.sum=x.sum+y.sum;
    k.lmax=max(x.lmax,x.sum+y.lmax);
    k.rmax=max(y.rmax,y.sum+x.rmax);
    k.maxn=max(max(x.maxn,y.maxn),x.rmax+y.lmax);
    k.val=(x.val&y.val);
    return k;
}

void build(int nd,int l,int r) {
    if(l==r) {
        tr[nd].val=a[l];
        tr[nd].lmax=tr[nd].rmax=tr[nd].maxn=tr[nd].sum=a[l];
        return ;
    }
    int mid=l+r>>1;
    build(nd<<1,l,mid); build(nd<<1|1,mid+1,r);
    tr[nd]=merge(tr[nd<<1],tr[nd<<1|1]);
}

void change(int nd,int l,int r,int x,int y,int k) {
    if(r<x||l>y) return ;
    if(l==r) {
        tr[nd].val=(tr[nd].val | k);
        tr[nd].lmax=tr[nd].maxn=tr[nd].rmax=tr[nd].sum=tr[nd].val;
        return ;
    }
    int mid=l+r>>1;
    if((tr[nd<<1].val&k)!=k) change(nd<<1,l,mid,x,y,k);
    if((tr[nd<<1|1].val&k)!=k) change(nd<<1|1,mid+1,r,x,y,k);
    tr[nd]=merge(tr[nd<<1],tr[nd<<1|1]);
}

node query(int nd,int l,int r,int x,int y) {
    if(l>=x&&r<=y) return tr[nd];
    int mid=l+r>>1,flag=0;
    node s;
    if(mid>=x) {
        node k=query(nd<<1,l,mid,x,y);
        flag=1; s=k;
    }
    if(mid+1<=y) {
        node k=query(nd<<1|1,mid+1,r,x,y);
        if(!flag) s=k;
        else s=merge(s,k);
    }
    return s;
}

int main() {
    n=read(); m=read();
    for(int i=1;i<=n;i++) {
        a[i]=read();
    }
    build(1,1,n);
    while(m--) {
        int op=read(),l=read(),r=read(),k;
        if(op==1) {
            node ans=query(1,1,n,l,r);
            cout<<max((LL)0,ans.maxn)<<'\n';
        }
        else {
            k=read();
            change(1,1,n,l,r,k);
        }
    }

    return 0;
}

CF438D The Child and Sequence

同样考虑一个数最多模几次。

考虑一个数 \(x\),若 \(x\mod y\)\(y\ge\frac{x}{2}\),那么结果小于 \(\frac{x}{2}\),若 \(y\le\frac{x}{2}\),结果也会小于 \(\frac{x}{2}\)

所以,一个数字最多模 \(\log a\) 次,维护区间是否全部为 \(1\),不是则直接暴力修改即可。

时间复杂度 \(O(m\log a\log n)\)

点击查看代码
#include<bits/stdc++.h>
using namespace std;

typedef long long LL;
typedef unsigned long long ULL;
LL read() {
    LL sum=0,flag=1; char c=getchar();
    while(c<'0'||c>'9') {if(c=='-') flag=-1; c=getchar();}
    while(c>='0'&&c<='9') {sum=sum*10+c-'0'; c=getchar();}
    return sum*flag;
}

const int N=1e5+10;
int n,m;
struct node{
    LL sum,maxn;
}tr[N<<2];

void pushup(int nd) {
    tr[nd].sum=tr[nd<<1].sum+tr[nd<<1|1].sum;
    tr[nd].maxn=max(tr[nd<<1].maxn,tr[nd<<1|1].maxn);
}

void change1(int nd,int l,int r,int x,int k) {
    if(l>x||r<x) return ;
    if(l==r) {
        tr[nd].sum=k;
        tr[nd].maxn=k;
        return ;
    }
    int mid=l+r>>1;
    change1(nd<<1,l,mid,x,k);
    change1(nd<<1|1,mid+1,r,x,k);
    pushup(nd);
}

void change2(int nd,int l,int r,int x,int y,int k) {
    if(l>y||r<x) return ;
    if(l==r) {
        tr[nd].sum%=k;
        tr[nd].maxn%=k;
        return ;
    }
    int mid=l+r>>1;
    if(tr[nd<<1].maxn>=k) change2(nd<<1,l,mid,x,y,k);
    if(tr[nd<<1|1].maxn>=k) change2(nd<<1|1,mid+1,r,x,y,k);
    pushup(nd);
}

LL query(int nd,int l,int r,int x,int y) {
    if(r<x||l>y) return 0;
    if(l>=x&&r<=y) return tr[nd].sum;
    int mid=l+r>>1;
    return query(nd<<1,l,mid,x,y)+query(nd<<1|1,mid+1,r,x,y);
}

int main() {
    n=read(); m=read();
    for(int i=1;i<=n;i++) {
        int x=read();
        change1(1,1,n,i,x);
    }
    while(m--) {
        int opt=read(),l=read(),r=read(),x;
        if(opt==1) {
            cout<<query(1,1,n,l,r)<<'\n';
        }
        else if(opt==2) {
            x=read();
            change2(1,1,n,l,r,x);
        }
        else {
            change1(1,1,n,l,r);
        }
    }

    return 0;
}
posted @ 2023-07-26 16:53  2017BeiJiang  阅读(12)  评论(0编辑  收藏  举报