【小结】可持久化线段树

可持久化线段树是能维护历史版本的线段树。

可以通过历史版本维护历史版本、区间等信息。

它本质上是线段树的前缀和操作。

可持久化值域线段树也简称主席树

P3919 【模板】可持久化线段树 1(可持久化数组)

题目大意:单点修改。查询历史版本上的某个值。

首先暴力的想法是可以每一次都修改,然后新建一棵线段树。

但是这样空间和时间都不够。不如暴力。

所以每一次单点修改,考虑有些值的修改是不是多余的?

我们发现:对于一个点,仅有 \(O(logn)\) 个线段树节点与它有关。

所以每一次修改只需要改这 \(logn\) 个点。

考虑动态开点线段树的想法,每一次新建一个节点,然后这个节点,因为是单点修改,肯定会有一边是不变的。

所以只修改另一边。另一边再如此,不断递归下去\(\dots\)

然后直到修改到叶子。就可以停下。每一次修改记录根节点,就可以啦!

然后查询的时候直接从 \(root_{i}\) 查询即可。

#include<bits/stdc++.h>
using namespace std;
/*

*/
struct FSI{
	template<typename T>
	FSI& operator >> (T &res){
		res=0;T f=1;char ch=getchar();
		while (!isdigit(ch)){if (ch=='-') f=-1;ch=getchar();}
		while (isdigit(ch)){res=res*10+ch-'0';ch=getchar();}
		res*=f;
		return *this;
	}
}scan;
const int N=1e6+10;
int n,m,i,w[N];
int idx;
int root[N];
int ver,op,x,y;
struct Node{
	int l,r,lc,rc,val;
}a[N*25];
int build(int l,int r)
{
	int p=++idx;
	a[p].l=l;
	a[p].r=r;
	if (l==r)
	{
		a[p].val=w[l];
		return p;
	}
	int mid=l+r>>1;
	a[p].lc=build(l,mid);
	a[p].rc=build(mid+1,r);
	return p;
}
int update(int k,int x,int v)
{
	int l=a[k].l,r=a[k].r;
	int p=++idx;
	a[p]=a[k];
	if (l==r)
	{
		a[p].val=v;
		return p;
	}
	int mid=l+r>>1;
	if (x<=mid) a[p].lc=update(a[k].lc,x,v);
	else a[p].rc=update(a[k].rc,x,v);
	return p;
}
int query(int k,int x)
{
	int l=a[k].l,r=a[k].r;
	if (l==r) return a[k].val;
	int mid=l+r>>1;
	if (x<=mid) return query(a[k].lc,x);
	else return query(a[k].rc,x);
}
int main()
{
	scan>>n>>m;
	for (i=1;i<=n;i++) scan>>w[i];
	root[0]=build(1,n);
	for (i=1;i<=m;i++)
	{
		scan>>ver>>op>>x;
		if (op==1) 
		{
			scan>>y;
			root[i]=update(root[ver],x,y);
		}
		else
		{
			printf("%d\n",query(root[ver],x));
			root[i]=root[ver];
		}
	}
	return 0;
}

然后就是经典的主席树。

P3834 【模板】可持久化线段树 2

经典的静态区间 \(k\) 大问题。

考虑值域线段树。

先考虑 \(n\) 个数的时候怎么办。

可以直接查询 \(k\) 大(类似线段树二分)。

那么这题加上了区间的限制。

所以可以维护 \(n\) 个历史版本,每次加入一个数,然后查询的时候,类似前缀和的计算区间内数的个数。

如此值域线段树上二分即可。

#include<bits/stdc++.h>
using namespace std;
/*

*/
struct FSI{
	template<typename T>
	FSI& operator >> (T &res){
		res=0;T f=1;char ch=getchar();
		while (!isdigit(ch)){if (ch=='-') f=-1;ch=getchar();}
		while (isdigit(ch)){res=res*10+ch-'0';ch=getchar();}
		res*=f;
		return *this;
	}
}scan;
const int N=2e5+10,L=33;
int n,m,i,w[N],l,r,K,b[N],k,root[N],idx;
struct Node{
	int l,r,sum,lc,rc;
}a[N*L];
void pushup(int k)
{
	a[k].sum=a[a[k].lc].sum+a[a[k].rc].sum;
}
int build(int l,int r)
{
	int p=++idx;
	a[p].l=l;
	a[p].r=r;
	if (l==r)
	{
		a[p].sum=0;
		return p;
	}
	int mid=l+r>>1;
	a[p].lc=build(l,mid);
	a[p].rc=build(mid+1,r);
	pushup(p);
	return p;
}
int update(int k,int x,int v)
{
	int p=++idx;
	a[p]=a[k];
	int l=a[p].l,r=a[p].r;
	if (l==x&&r==x)
	{
		a[p].sum+=v;
		return p;
	}
	int mid=l+r>>1;
	if (x<=mid) a[p].lc=update(a[k].lc,x,v);
	else a[p].rc=update(a[k].rc,x,v);
	pushup(p);
	return p;
}
int query(int L,int R,int k)
{
	int l=a[L].l,r=a[L].r;
	if (l==r) return l;
	int cnt=a[a[R].lc].sum-a[a[L].lc].sum;
	if (k<=cnt) return query(a[L].lc,a[R].lc,k);
	else return query(a[L].rc,a[R].rc,k-cnt);
}
int main()
{
	scan>>n>>m;
	for (i=1;i<=n;i++) scan>>w[i],b[++k]=w[i];
	sort(b+1,b+k+1);
	k=unique(b+1,b+k+1)-b-1;
	root[0]=build(1,k);
	for (i=1;i<=n;i++) w[i]=lower_bound(b+1,b+k+1,w[i])-b;
	for (i=1;i<=n;i++) root[i]=update(root[i-1],w[i],1);
	while (m--)
	{
		scan>>l>>r>>K;
		printf("%d\n",b[query(root[l-1],root[r],K)]);
	}
	return 0;
}

这启示我们:可持久化线段树可以帮助我们处理区间问题。

P3567 [POI 2014] KUR-Couriers

一个数,在一个区间内,出现次数超过一半,那么也就是出现次数 \(>\lfloor \frac{r-l+1}{2} \rfloor\)

如果让相同的数连续,那么这个数一定占据了第 \(\lfloor \frac{r-l+1}{2} \rfloor+1\) 个数。所以排序之后也会占据这个位置。

那么直接查询第 \(\lfloor \frac{r-l+1}{2} \rfloor+1\) 小的数。然后只有这个数有可能 成为众数。

最后看一下出现的次数,如果真的满足条件,那么就是。否则整个序列中一定不存在这样的数。

#include<bits/stdc++.h>
using namespace std;
/*

*/
struct FSI{
	template<typename T>
	FSI& operator >> (T &res){
		res=0;T f=1;char ch=getchar();
		while (!isdigit(ch)){if (ch=='-') f=-1;ch=getchar();}
		while (isdigit(ch)){res=res*10+ch-'0';ch=getchar();}
		res*=f;
		return *this;
	}
}scan;
const int N=5e5+10,L=24;
int T,n,m,i,w[N];
int root[N],idx;
int l,r,x;
struct Node{
	int l,r,sum,lc,rc;
}a[N*L];
void pushup(int k)
{
	a[k].sum=a[a[k].lc].sum+a[a[k].rc].sum;
}
int build(int l,int r)
{
	int p=++idx;
	a[p].l=l;
	a[p].r=r;
	if (l==r)
	{
		a[p].sum=0;
		return p;
	}
	int mid=l+r>>1;
	a[p].lc=build(l,mid);
	a[p].rc=build(mid+1,r);
	pushup(p);
	return p;
}
int update(int k,int x,int v)
{
	int p=++idx;
	int l=a[k].l,r=a[k].r;
	a[p]=a[k];
	if (l==x&&r==x)
	{
		a[p].sum+=v;
		return p;
	}
	int mid=l+r>>1;
	if (x<=mid) a[p].lc=update(a[k].lc,x,v);
	else a[p].rc=update(a[k].rc,x,v);
	pushup(p);
	return p;
}
int kth(int L,int R,int x)
{
	int l=a[L].l,r=a[L].r;
	if (l==r) return l;
	int cnt=a[a[R].lc].sum-a[a[L].lc].sum;
	if (cnt>=x) return kth(a[L].lc,a[R].lc,x);
	else return kth(a[L].rc,a[R].rc,x-cnt);
}
int query(int L,int R,int x)
{
	int l=a[L].l,r=a[L].r;
	if (l==x&&r==x) return a[R].sum-a[L].sum;
	int mid=l+r>>1;
	if (x<=mid) return query(a[L].lc,a[R].lc,x);
	else return query(a[L].rc,a[R].rc,x);
}
int main()
{
	scan>>n>>m;
	for (i=1;i<=n;i++) scan>>w[i];
	root[0]=build(1,n);
	for (i=1;i<=n;i++) root[i]=update(root[i-1],w[i],1);
	while (m--)
	{
		scan>>l>>r;
		x=kth(root[l-1],root[r],(r-l+1)/2+1);
		if (query(root[l-1],root[r],x)>(r-l+1)/2) printf("%d\n",x);
		else printf("0\n");
	}
	return 0;
}

P3963 [TJOI2013] 奖学金

考虑这种题,可以直接枚举中位数。因为中位数显然是数组中的数。

\(a\) 从小到大排序,那么要取的也就是前面的 \(\frac{n-1}{2}\) 个和后面的 \(\frac{n-1}{2}\) 个。

考虑前面,后面是对称的。

做法一:主席树

可以主席树查询出 \(x\) 使得 \(x\) 是第 \(\frac{n-1}{2}\) 小的数。然后算出和 \(S\) 和个数 \(C\)\(C\) 多出的部分都是 \(x\)。所以最后的答案是 \(S-(C-\frac{n-1}{2})\times x\)

做法二:普通值域线段树

容易发现,这个只用查询前缀或后缀。那么主席树的“前缀和”就失效了。我们只需要这个“前缀”。

所以直接按上面的步骤插入即可。

做法三:平衡树

把做法一查询 \(\frac{n-1}{2}\) 小的部分用平衡树替换。

做法四:优先队列

最简单的做法。

可以直接加入一个数,然后删除最大的那个。在此过程中维护和 \(sum\)

记录 \(pre_i\)\(nxt_i\) 然后判断即可。

这是代码中的做法。

启示:可持久化线段树擅长处理双区间或单区间类的二位偏序问题。而莫队擅长处理单区间问题。树状数组擅长处理普通的二维偏序问题。

#include<bits/stdc++.h>
using namespace std;
/*
首先按 a[] 从小到大排序。
那么需要选出 n 个数,使得中位数最大,也就等价于:第 (n+1)/2 个数。
那么枚举这个中位数。左边必须要选 n/2 个,右边也要选 n/2 个。
所以问题等价于求:一段前缀的最小的 k 个数的和。
那么可持久化线段树记录前缀的区间和,记录 num 和 sum 两个参数。
然后前后缀线段树二分即可。
*/
struct FSI{
	template<typename T>
	FSI& operator >> (T &res){
		res=0;T f=1;char ch=getchar();
		while (!isdigit(ch)){if (ch=='-') f=-1;ch=getchar();}
		while (isdigit(ch)){res=res*10+ch-'0';ch=getchar();}
		res*=f;
		return *this;
	}
}scan;
#define int long long
const int N=2e5+10,L=33,m=1e5;
int n,c,i,root[N],f;
int idx,sum,ans,pre[N],nxt[N];
priority_queue<int> q;
struct Node{
	int a,b;
}d[N];
bool cmp(Node x,Node y){return x.a<y.a;}
signed main()
{
	scan>>n>>c>>f;
	for (i=1;i<=c;i++) scan>>d[i].a>>d[i].b;
	sort(d+1,d+c+1,cmp);
	for (i=1;i<=c;i++)
	{
		if (q.size()>n/2) sum-=q.top(),q.pop();
		pre[i]=sum;
		q.push(d[i].b);
		sum+=d[i].b;
	}
	while (!q.empty()) q.pop();
	sum=0;
	for (i=c;i>=1;i--)
	{
		if (q.size()>n/2) sum-=q.top(),q.pop();
		nxt[i]=sum;
		q.push(d[i].b);
		sum+=d[i].b;
	}
	ans=-1;
	for (i=n/2+1;i<=c-n/2;i++)
	{
		if (pre[i]+nxt[i]+d[i].b<=f) ans=d[i].a;
	}
	printf("%lld",ans);
	return 0;
}

P2468 [SDOI2010] 粟粟的书架

好题。

考虑特殊的数据范围。

对于 \(50\%\) 的数据,\(R,C\leq 200,M\leq 2\times 10^5\)

对于另外 \(50\%\) 的数据,\(R=1,C\leq 5\times 10^5,M\leq 2\times 10^4\)

显然,这两组拼起来就是正解。

第一部分:\(R,C\leq 200,M\leq 2\times 10^5\)

考虑 \(M\) 非常大,看起来只能有一大堆的预处理加上带 \(log\) 的询问。

那么这里有一个常见处理方法。

\(sum_{i,j,k}\) 表示 \((1,1)\)\((i,j)\) 中数值 \(\geq k\) 的数之和。

同理,令 \(cnt_{i,j,k}\) 表示 \((1,1)\)\((i,j)\) 中数值 \(\geq k\) 的数的个数。

那么就可以二分 \(mid\) 表示数的下限,然后查询 \(\geq mid\) 的数的和是否 \(\geq H\)

找到这个数 \(x\)。考虑会有一些相同的值,被多计入到答案中。所以考虑扣除这些数。因为比 \(x\) 大的数都不合法,所以一定是删 \(x\) 且不可能把 \(x\) 全部删光。

设目前的这个和为 \(S\),个数为 \(C\),那么最终的个数为 \(C-\lfloor\frac{S-H}{x}\rfloor\)

第二部分:\(R=1,C\leq 5\times 10^5,M\leq 2\times 10^4\)

这部分就是一个一维的数组问题。给出的是数组,查询的是区间。

设要找 \([l,r]\) 中的东西。

可持久化线段树维护第 \(i\) 个版本。

然后直接主席树上二分找出 \(x\)\(x\) 为最大的使得和大于等于 \(H\) 的数。

我们现在找出了 \(x\)

那么再用两个函数分别查询 \([l,r]\)\(S\)\(C\)

这样,公式还是 \(C-\lfloor\frac{S-H}{x}\rfloor\)

直接输出即可。

#include<bits/stdc++.h>
using namespace std;
/*
HACK:
5 5 1
14 15 9 26 53
58 9 7 9 32
38 46 26 43 38
32 7 9 50 28
8 41 9 7 17
1 2 5 3 139
*/
struct FSI{
	template<typename T>
	FSI& operator >> (T &res){
		res=0;T f=1;char ch=getchar();
		while (!isdigit(ch)){if (ch=='-') f=-1;ch=getchar();}
		while (isdigit(ch)){res=res*10+ch-'0';ch=getchar();}
		res*=f;
		return *this;
	}
}scan;
const int N=210,M=1010,K=5e5+10,L=33;
int n,m,q,i,j,k,sx,sy,tx,ty,H;
int sum[N][N][M],cnt[N][N][M],w[N][N],c[K];
int l,r,mid;
int C,S;
int root[K],idx,x;
struct Node{
	int l,r,lc,rc,sum,num;
}a[K*L];
int calcsum(int sx,int sy,int tx,int ty,int k)
{
	return sum[tx][ty][k]-sum[sx-1][ty][k]-sum[tx][sy-1][k]+sum[sx-1][sy-1][k];
}
int calccnt(int sx,int sy,int tx,int ty,int k)
{
	return cnt[tx][ty][k]-cnt[sx-1][ty][k]-cnt[tx][sy-1][k]+cnt[sx-1][sy-1][k];
}
void pushup(int k)
{
	a[k].sum=a[a[k].lc].sum+a[a[k].rc].sum;
	a[k].num=a[a[k].lc].num+a[a[k].rc].num;
}
int build(int l,int r)
{
	int p=++idx;
	a[p].l=l;
	a[p].r=r;
	if (l==r)
	{
		a[p].sum=a[p].num=0;
		return p;
	}
	int mid=l+r>>1;
	a[p].lc=build(l,mid);
	a[p].rc=build(mid+1,r);
	pushup(p);
	return p;
}
int update(int k,int x)
{
	int p=++idx;
	a[p]=a[k];
	int l=a[p].l,r=a[p].r;
	if (l==x&&r==x)
	{
		a[p].sum+=x;
		a[p].num++;
		return p;
	}
	int mid=l+r>>1;
	if (x<=mid) a[p].lc=update(a[k].lc,x);
	else a[p].rc=update(a[k].rc,x);
	pushup(p);
	return p;
}
int query(int L,int R,int H)
{
	int l=a[L].l,r=a[L].r;
	if (l==r) return l;
	int mid=l+r>>1;
	int sum=a[a[R].rc].sum-a[a[L].rc].sum;
	if (sum>=H) return query(a[L].rc,a[R].rc,H);
	else return query(a[L].lc,a[R].lc,H-sum);
}
int query_cnt(int k,int x,int y)
{
	int l=a[k].l,r=a[k].r;
	if (l>=x&&r<=y) return a[k].num;
	int mid=l+r>>1,res=0;
	if (x<=mid) res+=query_cnt(a[k].lc,x,y);
	if (y>mid) res+=query_cnt(a[k].rc,x,y);
	return res;
}
int query_sum(int k,int x,int y)
{
	int l=a[k].l,r=a[k].r;
	if (l>=x&&r<=y) return a[k].sum;
	int mid=l+r>>1,res=0;
	if (x<=mid) res+=query_sum(a[k].lc,x,y);
	if (y>mid) res+=query_sum(a[k].rc,x,y);
	return res;
}
void work1()
{
	n=m;
	for (i=1;i<=n;i++) scan>>c[i];
	root[0]=build(1,1002);
	for (i=1;i<=n;i++) root[i]=update(root[i-1],c[i]);
	while (q--)
	{
		scan>>sx>>l>>tx>>r>>H;
		x=query(root[l-1],root[r],H);
		C=query_cnt(root[r],x,1002)-query_cnt(root[l-1],x,1002);
		S=query_sum(root[r],x,1002)-query_sum(root[l-1],x,1002);
		if (S<H)
		{
			puts("Poor QLW");
			continue;
		}
		printf("%d\n",C-(S-H)/x);
	}
}
void work2()
{
	for (i=1;i<=n;i++)
	{
		for (j=1;j<=m;j++) scan>>w[i][j];
	}
	for (i=1;i<=n;i++)
	{
		for (j=1;j<=m;j++)
		{
			for (k=1002;k>=1;k--) 
			{
				sum[i][j][k]=sum[i-1][j][k]+sum[i][j-1][k]-sum[i-1][j-1][k]+(w[i][j]>=k?w[i][j]:0);
				cnt[i][j][k]=cnt[i-1][j][k]+cnt[i][j-1][k]-cnt[i-1][j-1][k]+(w[i][j]>=k?1:0);
			}
		}
	}
	while (q--)
	{
		scan>>sx>>sy>>tx>>ty>>H;
		l=1;
		r=1002;
		while (l<=r)
		{
			mid=l+r>>1;
			if (calcsum(sx,sy,tx,ty,mid)>=H) l=mid+1;  
			else r=mid-1;
		}
		//现在已经处理出了数量所在的那个数,所以它就可以是删除一些数。现在要来计算这些数。
		//那么现在这个数经过二分确定是 r,所以每一次删数必定是减去 r。
		//设目前的个数为 C,和为 S,那么答案为 C-(S-H)/r。
		if (!r)
		{
			puts("Poor QLW");
			continue;
		}
		S=calcsum(sx,sy,tx,ty,r);
		C=calccnt(sx,sy,tx,ty,r);
		printf("%d\n",C-(S-H)/r);
	}
}
int main()
{
	scan>>n>>m>>q;
	if (n==1) work1();
	else work2();
	return 0;
}

CF893F Subtree Minimum Query

遇到子树距离不超过 \(k\) 这种问题,可以用可持久化线段树。

考虑树上问题转为序列问题。首先这种题都是要转化为 dfn 序。

这样,“\(x\) 子树内” 这个限制就解决了。

只需要转化为“dfn 序在 \([dfn_x,dfn_x+sz_x-1]\)”即可。

接下来就是考虑距离的问题。

如果没有距离,那么可以直接静态区间线段树查询,甚至可以 ST 表查询。但是加了这个限制。

考虑可持久化线段树。按深度加入。令第 \(i\) 个版本表示 \(i\) 深度前的点加入之后的版本。

总共最多加入 \(n\) 次。所以时空复杂度都是 \(O(nlogn)\) 的。

而且因为只查询了子树中的值,所以一定没有深度比它小的点。

可以通过。

特别注意一个细节:在加入的时候一定要每加一个点就新建一个根,然后覆盖掉原来版本的根。绝不能分类讨论是否为第一个点,第一次新建根,后面直接在根上修改。这样不仅不方便(要写两个函数),而且会错误的修改前面版本的值(因为有些部分与前面是公用的)。所以一定要用第一种写法!

再此对比一下。

Accepted

WA on #43

放上代码:

#include<bits/stdc++.h>
using namespace std;
/*
直接按深度插入。
对于一个深度 dep,查询的就是 [dep,dep+k-1] 的最小值。

*/
struct FSI{
	template<typename T>
	FSI& operator >> (T &res){
		res=0;T f=1;char ch=getchar();
		while (!isdigit(ch)){if (ch=='-') f=-1;ch=getchar();}
		while (isdigit(ch)){res=res*10+ch-'0';ch=getchar();}
		res*=f;
		return *this;
	}
}scan;
const int N=5e5+10,L=33,inf=1e9+10;
int n,m,r,i,w[N],x,y,k;
int last[N],c,dep[N],sz[N];
int dfn[N],stamp;
int idx;
int maxdep,root[N];
int t;
vector<int> G[N];
struct Edge{
	int to,next;
}e[N<<1];
struct Node{
	int l,r,lc,rc,mi;
}a[N*L];
void add(int u,int v)
{
	e[++c]={v,last[u]};
	last[u]=c;
}
void dfs(int x,int fa)
{
	int i,to;
	sz[x]=1;
	dfn[x]=++stamp;
	dep[x]=dep[fa]+1;
	G[dep[x]].push_back(x);
	maxdep=max(maxdep,dep[x]);
	for (i=last[x];i;i=e[i].next)
	{
		to=e[i].to;
		if (to==fa) continue;
		dfs(to,x);
		sz[x]+=sz[to];
	}
}
void pushup(int k)
{
	a[k].mi=min(a[a[k].lc].mi,a[a[k].rc].mi);
}
int build(int l,int r)
{
	int p=++idx;
	a[p].l=l;
	a[p].r=r;
	if (l==r)
	{
		a[p].mi=inf;
		return p;
	}
	int mid=l+r>>1;
	a[p].lc=build(l,mid);
	a[p].rc=build(mid+1,r);
	pushup(p);
	return p;
}
int update(int k,int x,int v)
{
	int p=++idx;
	a[p]=a[k];
	int l=a[p].l,r=a[p].r;
	if (l==x&&r==x)
	{
		a[p].mi=min(a[p].mi,v);
		return p;
	}
	int mid=l+r>>1;
	if (x<=mid) a[p].lc=update(a[k].lc,x,v);
	else a[p].rc=update(a[k].rc,x,v);
	pushup(p);
	return p;
}
/*
void motify(int k,int x,int v)//千万不要写这个函数!
{
	int l=a[k].l,r=a[k].r;
	if (l==x&&r==x)
	{
		a[k].mi=min(a[k].mi,v);
		return;
	}
	int mid=l+r>>1;
	if (x<=mid) motify(a[k].lc,x,v);
	else motify(a[k].rc,x,v);
	pushup(k);
}
*/
int query(int k,int x,int y)
{
	int l=a[k].l,r=a[k].r;
	if (l>=x&&r<=y) return a[k].mi;
	int mid=l+r>>1,res=inf;
	if (x<=mid) res=min(res,query(a[k].lc,x,y));
	if (y>mid) res=min(res,query(a[k].rc,x,y));
	return res;
}
int main()
{
	scan>>n>>r;
	for (i=1;i<=n;i++) scan>>w[i];
	for (i=1;i<n;i++)
	{
		scan>>x>>y;
		add(x,y);
		add(y,x);
	}
	dfs(r,0);
	root[0]=build(1,stamp);
	for (i=1;i<=maxdep;i++)
	{
		for (int x:G[i])
		{
//这下面就是我错的地方,else 下面不能写 motify。
			if (!root[i]) root[i]=update(root[i-1],dfn[x],w[x]);
			else root[i]=update(root[i],dfn[x],w[x]);
		}
	}
	scan>>m;
	while (m--)
	{
		scan>>x>>k;
		x=(x+t)%n+1;
		k=(k+t)%n;
		t=query(root[min(dep[x]+k,maxdep)],dfn[x],dfn[x]+sz[x]-1);
		printf("%d\n",t);
	}
	return 0;
}
posted @ 2026-08-21 16:47  GUO120822  阅读(1)  评论(0)    收藏  举报