莫队

先言

来一写一下莫队,最近 \(YJK\) 一直在给我拍砖,导致我和一个 \(SB\) 一样,我决定写一篇找不出毛病的博文。

普通莫队: 算法简介

算的上是一个暴力美学,排序加分块,所以准确的来说,会了分块的思想和 \(sort\) 一些基础知识,你就可以手玩莫队了。莫队解决的是离线区间问题。 下方的图片:

我们就可以得到 \(a,b,c\) 三个查询区间,那么我们按照左端点进行排序,我们就可以得到 \(a,b,c\) 三个查询区间整齐的排列的区间上,我们暂且先不管右端点如何,我们暂且只管左端点。

我们假设两个头尾指针 \(l,r\) ,我们从左往右走,在走到第 \(a\) 号区间,我们 \(l = 1\) 显然已经到了我们要求的区间了,那么我们可以让 \(r\) 去蹦了,一直蹦到 \(n-4\) 也就是我们 \(a\) 号区间的右端点,统计完答案,然后我们再让 \(l ++\) 去寻找,我们就找到了 \(c\) 号查询区间,那么我们还是让 \(r\) 去找,同时我们看一下到底 \(c.r\)\(r\) 的大小,然后让 \(r\) 逐步去接近它,最终得出答案。

最后就统计答案就行了 。
这就是普通莫队的流程了。

\(l\)\(r\) 的区间移动

卡莫队

  • 数字表示区间的左右端点
    我们很显然的发现,如果按照我们这个回路的我们让 \(1\)\(n\) ,让 \(2\to 3\)\(3 \to n\) 一直这么下去,很显然,我们发现这货是 \(O(n^2)\) 的。这个这个莫队的算法限制

优化

我这个 \(fw\) 只有两种优化方式

  • 1.\(register ,inline\) 并且把 \(add, del\) 操作给写到主函数里面去。可以能会优化 \(1S\)
  • 2.奇偶排序 : 我们按照其左端点所在的块进行排序。\(li\) 表示的是 \(l\) 所在的块
inline bool cmp1(Block a , Block b) {return a.li == b.li ? a.l < b.l : a.r < b.r ; }

普通莫队:例题 :小Z的袜子

我们把它当模板,它没把自己当模板,它把莫队卡了,

【description】:

求一个区间内每种颜色数目的平方和。

【solution】:

我们按照上述普通莫队的算法流程进行计算,我们同时我们开一个 \(cnt\) 记录一下当前的颜色数即可。 然后我们定义一个 \(ret\) 保证全局变量统计答案。

Code

没有办法 \(AC\) 我只有70分,写丑了

/*
 by : Zmonarch
 知识点 : 莫队 
 话说袜子分左右吗? 
*/
#include <iostream>
#include <cstdio>
#include <cstring>
#include <algorithm>
#include <queue>
#include <stack>
#include <map>
#include <set>
#include <cmath>
#include <vector>
#define int long long
#define inf 63
#define re register
const int kmaxn = 1e6 + 10 ; 
const int kmod = 1e9 + 7 ;
namespace Base 
{
	inline int Min(int a , int b) {return a < b ? a : b ; } 
	inline int Max(int a , int b) {return a > b ? a : b ; }
	inline int Abs(int a      ) {return a < 0 ? - a : a ; }  
	inline int gcd(int a , int b) {return !b ? a : gcd(b , a %b) ; }
}
inline int read()
{
	int x = 0 , f = 1 ; char ch = getchar() ; 
	while(!isdigit(ch)) {if(ch == '-') f = - 1 ; ch = getchar() ; }
	while( isdigit(ch)) {x = x * 10 + ch - '0' ; ch = getchar() ; }
	return x * f ; 
}
int num[kmaxn] , n , m , len , cnt[kmaxn] , l , r , ret ;  
struct Block
{
	 int l , r , ans1 , ans2 , li , pos ;
}block[kmaxn] ; 
inline bool cmp1(Block a , Block b) 
{
	return a.li == b.li ? a.l < b.l : a.r < b.r ; 
}
inline bool cmp2(Block a , Block b) 
{
	return a.pos < b.pos ; 
}
inline void add(int pos) 
{
	ret -= cnt[num[pos]] * cnt[num[pos]] ; 
	cnt[num[pos]]++; 
	ret += cnt[num[pos]] * cnt[num[pos]] ; 
}
inline void del(int pos) 
{
	ret -= cnt[num[pos]] * cnt[num[pos]] ; 
	cnt[num[pos]]-- ; 
	ret += cnt[num[pos]] * cnt[num[pos]] ; 
}
signed main() 
{
	n = read() , m = read() ; len = pow(n , 2/3) ; 
	for(re int i = 1 ; i <= n ; i++) num[i] = read() ; 
	for(re int i = 1 ; i <= m ; i++) 
	{
		block[i].l = read() , block[i].r = read() ; 
		block[i].pos = i ; block[i].li = l / len  ;
	}
	std::sort(block + 1 , block + m + 1 , cmp1) ; 
	l = 1 , r = 0 ; 
	for(re int i = 1 ; i <= m ; i++)
	{
		while(l < block[i].l) del(l++) ; 
		while(l > block[i].l) add(--l) ; 
		while(r < block[i].r) add(++r) ; 
		while(r > block[i].r) del(r--) ; 
		if(l == r) 
		{
			block[i].ans1 = 0 ; 
			block[i].ans2 = 1 ; 
			continue ; 
		}
		block[i].ans1 = ret - (r - l + 1 ) ; 
		block[i].ans2 = ( r - l + 1 ) * ( r - l ) ;  
		int g = Base::gcd(block[i].ans1 , block[i].ans2) ; 
		block[i].ans1 = block[i].ans1 / g ; 
		block[i].ans2 = block[i].ans2 / g ; 
	}
	std::sort(block + 1 , block + m + 1 , cmp2) ;//最简
	for(re int i = 1 ; i <= m ; i++) printf("%lld/%lld\n" , block[i].ans1 , block[i].ans2 ) ;  
	return 0 ; 
}

普通莫队:题单

SP3267 DQUERY - D-query
P2709 小B的询问
P1494 [国家集训队]小Z的袜子
P3709 大爷的字符串题
CF617E XOR and Favorite Number

带修莫队:算法简介 :

在普通莫队的基础上,我们推出了带修莫队,也就是带修改的莫队。

现在我们需要对区间进行修改,如果我们重新打乱,进行修改(首先这是在线,不怎么可取) ,我们直接记录 \(last\) 表示在这个时间内最近一次修改。

我们假设现在需要查询的区间为 \(l\to r\) ,那么我们需要修改的 \(pos\)\(l,r\) 有三种显然的关系

  • \(pos < l < r\) ,这么说的话,我们根本不需要在意这次的修改
  • \(l < r < pos\) ,同上
  • \(l < pos < r\) 也就是说,我们在这次的区间访问中有将该区间的元素修改的操作(我们通过在时间轴上的 \(l , r\) 区间的访问时间和 \(query\) 访问区间的时间),然后我们直接修改,同时我们进行修改的时候,到了那个地方再修改。

结合代码更好理解一些


\(update\) 操作就是更修改操作。

  • t 表示的是一个时间轴,我们把每一次修改操作都看成在 \(tot2\) 的时间内进行的一个 \(np\) 操作,然后我们将 \(t\) 赋值成最近的那一次, 然后我们在 \(update\) 的时候,进行一下判断,也就是看一下是否在这个区间内,然后进行一系列的操作,同时,因为修改了,所以讲你原来的值和修改的值换一换

例题:带修莫队:P1903数颜色

【description】:

给定两个操作,操作一是查询区间内有多少个不同的数字,操作二支持修改。

【solution】:

占个坑,我觉得我能写不少。

【Code】 :

/*
 by : Zmonarch
 知识点 : 带修莫队 
  
*/
#include <iostream> 
#include <cstdio> 
#include <cstring>
#include <cmath>
#include <algorithm>
#include <queue>
#include <stack>
#include <set>
#include <map>
#include <vector>
#define int long long
#define inf 63
#define re register
const int kmaxn = 1e6 + 10 ; 
const int kmod = 1e9 + 7 ;
namespace Base 
{
	inline int Min(int a , int b) {return a < b ? a : b ; } 
	inline int Max(int a , int b) {return a > b ? a : b ; }
	inline int Abs(int a      ) {return a < 0 ? - a : a ; }  
	inline int gcd(int a , int b) {return !b ? a : gcd(b , a %b) ; }
}
inline int read()
{
	int x = 0 , f = 1 ; char ch = getchar() ; 
	while(!isdigit(ch)) {if(ch == '-') f = - 1 ; ch = getchar() ; }
	while( isdigit(ch)) {x = x * 10 + ch - '0' ; ch = getchar() ; }
	return x * f ; 
}
int n , m , len ;
struct Block //记录询问 
{
	int l , r , t , pos , ans ; 
}block[kmaxn << 1] ; 
struct change //记录修改 
{
	int pos , val , id , t ; 
}query[kmaxn << 1] ;  
int num[kmaxn << 1] , ret , tot1 , tot2 , cnt[kmaxn << 1];
//tot1表示询问的区间,tot2表示询问 
inline void add(int pos) 
{
	ret += (++cnt[num[pos]] == 1)  ; 
} 
inline void del(int pos) 
{
	ret -= (--cnt[num[pos]] == 0) ; 
}
inline bool cmp1(Block a , Block b) 
{
	return (a.l/len ^ b.l/len) ?a.l/len < b.l/len : ((a.r/len ^ b.r/len) ? a.r/len < b.r/len : a.t < b.t );
}
inline bool cmp2(Block a , Block b) 
{
	return a.pos < b.pos ; 
}
inline void update(int pos , int t) 
{
	if(block[pos].l <= query[t].pos && query[t].pos <= block[pos].r) 
	{
		ret -= !--cnt[num[query[t].pos]] - !cnt[query[t].val]++ ;
	}
	std::swap(num[query[t].pos] , query[t].val ) ; 
}
signed main() 
{
	n = read() , m = read() ;  len = pow(n , (double)2.0/3.0) ; 
	//dalao说n^1/2会退化成n^2
	for(int i = 1 ; i <= n ; i++) num[i] = read() ; 
	for(int i = 1 ; i <= m ; i++) 
	{
		char s[5] ; scanf("%s" , s ) ;
		int l = read() , r = read() ;
		if(s[0] == 'Q') ++tot1 , block[tot1].pos = tot1 , block[tot1].l = l , block[tot1].r = r , block[tot1].t = tot2 ;
		if(s[0] == 'R') query[++tot2].pos = l , query[tot2].val = r ; 
	}
	std::sort(block + 1 , block + tot1 + 1 , cmp1) ;
	int l = 1 , r = 0 , t = 0 ; 
	for(int i = 1 ; i <= tot1 ; i++) 
	{
		while(l > block[i].l) add(--l) ; //ret += !cnt[num[--l] ]++; //
		while(l < block[i].l) del(l++) ; //ret -= !--cnt[num[l++]] ; //
		while(r > block[i].r) del(r--) ;//ret -= !--cnt[num[r--]] ;// 
		while(r < block[i].r) add(++r) ;//ret += !cnt[num[++r]]++ ;// 
		while(t < block[i].t) update(i , ++t) ; 
		while(t > block[i].t) update(i , t--) ; 
		block[i].ans = ret ; 
	}
	std::sort(block + 1 , block + tot1 + 1 , cmp2) ; 
	for(int i = 1 ; i <= tot1 ; i++) printf("%lld\n" , block[i].ans) ; 
	return 0 ; 
}

带修莫队:题单

CF940F Machine Learning

树上莫队:算法简介

我们如果学过树链剖分的话,我们就很显然的清楚,在一个树上,如果进行操作的话,我们会选择用树链剖分将整颗树剖下来。我们把树上剖下来的链看成区间,那么我们就有了莫队的资格。


以上述的图为例,我们看一下树上莫队。

首先我们先考虑不在同一颗子树内的时候,也就是是其中 \(f \to a\) 的简单路径,我们抽出来进行一下操作,我们搞出来的欧拉序中恰好从 \(a \to f\) 正好是我们要的结果,这可能是一种特例。

考虑在用一颗子树内的时候, 也就是其中 \(f\to e\) 的时候,我们继续寻找欧拉序,我们发现在 \(f\to e\) 的欧拉序中,如果出现两次一样的节点,那么这个点就绝不是简单路径 , \(f ,e\) 分别从第二个和第一个算起。(也就是没有重合的时候),这是欧拉序的性质使然,有兴趣的直接去查一下欧拉序吧。

我们发现其中没有 \(b\) 点,也就是没有 \(lca\) 这是什么原因? 在一个根节点为 \(now\) 的子树,它的子树的节点形成的欧拉序对于紧跟在 \(now\) 之后,并且在下一次 \(now\) 出现之前。

也就是 \(f\to e\) 这一条路径,因为全在 \(b\) 这一颗子树内,我们在欧拉序中是无法遍历到 \(b\) 这个点的,所以我们需要记录一下 \(lca\) 进行一下特判

树上莫队: 例题:SP10707 COT2 - Count on a tree II

【description】 :

占个坑,有空回来补,有些遗忘了

【Solution】:

Code :

/*
 by : Zmonarch
 知识点 :

*/
#include <iostream>
#include <cstdio>
#include <algorithm>
#include <cmath>
#include <queue>
#include <stack>
#include <set>
#include <cstring>
#include <map>
#include <cstdlib>
#define int long long
#define inf 2147483647
const int kmaxn = 1e6 + 10 ;
const int kmod = 1e9 + 7 ; 
namespace base
{
	inline int Min(int a , int b) { return a < b ? a : b ; } ;
	inline int Max(int a , int b) { return a > b ? a : b ; } ;
	inline int Abs(int a      ) { return a < 0 ? - a : a ; } ;
};
inline int read()
{
	int x = 0 , f = 1 ; char ch = getchar() ;
	while(!isdigit(ch)) { if(ch == '-') f = - 1 ; ch = getchar() ; }
	while( isdigit(ch)) { x = x * 10 + ch - '0' ; ch = getchar() ; }
	return x * f ;
}
using namespace base ;
struct Block 
{
	int l , r , li , ri , ans , lca , pos ; 
}block[kmaxn << 1] ; 
int n , m , ret , sum , len ;
int f[kmaxn << 1] , dep[kmaxn << 1] , top[kmaxn << 1] , son[kmaxn << 1] , size[kmaxn << 1] , st[kmaxn << 1] ;
int ed[kmaxn << 1] , num[kmaxn << 1] , id[kmaxn << 1] , used[kmaxn << 1] , cnt[kmaxn << 1] , b[kmaxn << 1];    
bool cmp1(Block a , Block b) //OK
{
	return (a.li == b.li) ? (a.li & 1) ? a.r < b.r : a.r > b.r : a.l < b.l ; 
}
bool cmp2(Block a , Block b) 
{
	return a.pos < b.pos ; 
}
struct node 
{
	int nxt , u ,v , val ; 
}e[kmaxn << 1] ; 
int tot , h[kmaxn << 1] ; 
void add(int u , int v) 
{
	e[++tot].nxt = h[u] ; 
	e[tot].u = u ; 
	e[tot].v = v ; 
	h[u] = tot ; 
}
void add(int pos) //OK
{
	ret += (++cnt[num[pos]] == 1) ; 
}
void del(int pos) //ok
{
	ret -= (--cnt[num[pos]] == 0) ; 
}
void check(int pos) //检查一下是否应该在莫队中,也就是是否在同一条链上。   // OK
{
	if(!used[pos]) add(pos) ; 
	else del(pos) ; 
	used[pos] ^= 1 ; 
}
void dfs1(int  u , int fa) //第一个DFS求解深度和子树大小和重儿子 
{
	f[u] = fa ; 
	st[u] = ++sum ; //记录入栈顺序 
	id[sum] = u ;
	size[u] = 1 ; 
	dep[u] = dep[fa] + 1 ; 
	for(int i = h[u] ; i ; i = e[i].nxt) 
	{
		int v = e[i].v ; 
		if(v == fa) continue ; 
		dfs1(v , u) ; 
		size[u] += size[v] ; 
		if(size[v] > size[son[u]]) son[u] = v ;
	} 
	ed[u] = ++ sum ; id[sum] = u ; //记录出栈顺序,并且记录u的欧拉序 
}
void dfs2(int u , int topp ) //剖链 
{
	top[u] = topp ; 
	if(son[u]) dfs2(son[u] , topp) ; 
	for(int i = h[u] ; i ; i = e[i].nxt ) 
	{
		int v = e[i].v ; 
		if(v == f[u] || v == son[u]) continue ; 
		dfs2(v , v) ; 
	}
}
int getlca(int u , int v) 
{
	while(top[u] != top[v]) 
	{
		if(dep[top[u]] >= dep[top[v]]) u = f[top[u]] ; 
		else v = f[top[v]] ; 
	}
	return dep[u] < dep[v] ? u : v ; 
} 
signed main()
{
	n = read() , m = read() ; len = sqrt(n) ; 
	for(int i = 1 ; i <= n ; i++) num[i] = read() , b[i] = num[i] ; 
	std::sort(b + 1 , b + n + 1) ; 
	for(int i = 1 ; i <= n ; i++) 
	num[i] = std::lower_bound(b + 1 , b + n + 1 , num[i]) - b ; 
	for(int i = 1 ; i <= n - 1 ; i++) 
	{
		int u = read() , v = read() ; 
		add(u , v) , add(v , u) ; 
	}
	dfs1(1 , 0) ; 
	/*for(int i = 1 ; i <= n ; i++) printf("test %lld %lld %lld\n" , st[i] , ed[i] , dep[i]) ; */
	dfs2(1 , 1) ; 
	for(int i = 1 ; i <= m ; i++) 
	{
		int u = read() , v = read() ; if(st[u] > st[v]) std::swap(u , v) ;
		block[i].pos = i ; block[i].lca = getlca(u , v) ; 
		if(block[i].lca == u)
		{
			block[i].l = st[u] ; 
			block[i].r = st[v] ;
			block[i].lca = 0 ; //后边需要特判一下没有遍历到LCA的情况 
		}
		else 
		{
			block[i].l = ed[u] ; 
			block[i].r = st[v] ; 
		}
		block[i].li = block[i].l / len ; 
		block[i].ri = block[i].r / len ; 
	}
	std::sort(block + 1 , block + m + 1 , cmp1) ;
	int l = 1 , r = 0 ;   
	for(int i = 1 ; i <= m ; i++) 
	{
		while(l < block[i].l) check(id[l++]) ; 
		while(l > block[i].l) check(id[--l]) ;
		
		while(r < block[i].r) check(id[++r]) ; 
		while(r > block[i].r) check(id[r--]) ;
		if(block[i].lca) check(block[i].lca) ;
		block[i].ans = ret ; 
	//	printf("ret : %lld\n" , ret) ; 
		if(block[i].lca) check(block[i].lca) ; 
	}
	std::sort(block + 1 , block + m + 1 , cmp2) ; 
	for(int i = 1 ; i <= m ; i++) printf("%lld\n" , block[i].ans) ; 
	return 0 ;
} 

题单:树上莫队

P4689 [Ynoi2016] 这是我自己的发明
P4074 [WC2013] 糖果公园

回滚莫队: 算法简介

首先我们按照区间左端点所在的块进行排序,同时按照 \(r\) 为第二关键字排序,然后我们就可以得到一个左端点漂浮不定,右端点递增,但整体看起开递增的一个不错的询问。

我们在进行操作的时候,普通没有什么两样,但是在进行查询区间的时候,我们利用分块的思想,我们看一下我们询问的区间是否是在一个块内,如果在一个块内的话, 我们就可以直接 \(O(\sqrt n)\) 的暴力进行求解,同时对于不是在用一个块内,我们先进行求解右端点,因为右端点是递增的,我们这次通过这个右端点求出答案对后面是有贡献的。但是我们这个左端点是一丢丢的贡献都没有,所以我们最后求解完成这个左节点,我们把答案归还,然后下一个区间的时候,在让他自己去向前找左端点。 这就叫做回 - 滚

回滚莫队:例题:[P5906 【模板】回滚莫队&不删除莫队

](https://www.luogu.com.cn/problem/P5906)

solution

和上述算法流程讲述一致 ,我们分别计入两个 \(ma ,st\) 表示头尾,然后正常的回滚莫队即可。

Code

/*
 By : Zmonarch
 知识点 :

*/
#include <iostream>
#include <cstdio>
#include <algorithm>
#include <cmath>
#include <queue>
#include <stack>
#include <cstring>
#include <vector>
#include <map>
#include <set>
#define int long long
#define inf 2147483647
#define qwq register
const int kmaxn = 1e6 + 10 ;
const int kmod = 998244353 ;
namespace Base
{
	inline int Min(int a , int b) {return a < b ? a : b ;}
	inline int Max(int a , int b) {return a < b ? b : a ;}
	inline int Abs(int a) {return a < 0 ? - a : a ;}
	inline int Gcd(int a , int b) {return !b ? a : Gcd(b , a % b) ;}
}
inline int read()
{
	int x = 0 , f = 1 ; char ch = getchar() ;
	while(!isdigit(ch)) {if(ch == '-') f = - 1 ; ch = getchar() ; }
	while( isdigit(ch)) {x = x * 10 + ch - '0' ; ch = getchar() ; }
	return x * f ;
}
int n , m , Max , len , ret ; 
int num[kmaxn] , b[kmaxn] , cnt[kmaxn] , belong[kmaxn] , st[kmaxn] , ma[kmaxn] , clear[kmaxn]; 
struct Block 
{
	int l , r , id , ans ; 
}block[kmaxn] ; 
inline int calc(int l , int r) 
{
	static int tim[kmaxn] ; int ans = 0 ; 
	for(qwq int i = l ; i <= r ; i++) tim[num[i]] = 0 ; 
	for(qwq int i = l ; i <= r ; i++) 
	 if(!tim[num[i]]) tim[num[i]] = i ; 
	 else ans = Base::Max(ans , i - tim[num[i]]) ; 
	return ans ;
}
inline int work(int i , int id) 
{
	int R = Base::Min(n , id * len) , l = R + 1 , r = l - 1 ; ret = 0 ; 
	int sum = 0 ; 
	memset(cnt , 0 , sizeof(cnt)) ;
	for(; belong[block[i].l] == id ; i++) 
	{
		if(belong[block[i].l] == belong[block[i].r]) 
		{block[i].ans = calc(block[i].l , block[i].r) ; continue ;}
		while(r < block[i].r)
		{
			r++ ; ma[num[r]] = r ; 
			if(!st[num[r]]) st[num[r]] = r , clear[++sum] = num[r] ; 
			ret = Base::Max(ret , r - st[num[r]]) ;	
		}  
		int cur = ret ; 
		while(l > block[i].l) 
		{
			l-- ; 
			if(ma[num[l]]) ret = Base::Max(ret , ma[num[l]] - l) ;
			else ma[num[l]] = l ; 
		} 
		block[i].ans = ret ; 
		while(l <= R) {if(ma[num[l]] == l) ma[num[l]] = 0 ; l++;} 
		ret = cur ; 
	} 
	for(qwq int i = 1 ; i <= sum ; i++) ma[clear[i]] = st[clear[i]] = 0 ; 
	return i ; 
}
inline bool cmp1(Block a , Block b) {return belong[a.l] ^ belong[b.l] ? belong[a.l] < belong[b.l] : a.r < b.r ;}
inline bool cmp2(Block a , Block b) {return a.id < b.id ;}
signed main()
{
	n = read() ; len = sqrt(n) ; 
	for(qwq int i = 1 ; i <= n ; i++) 
	num[i] = read() , b[i] = num[i] , belong[i] = (i - 1) / len + 1 , Max = Base::Max(Max , belong[i]);
	std::sort(b + 1 , b + n + 1) ; 
	int tot = std::unique(b + 1 , b + n + 1) - b - 1 ; 
	for(qwq int i = 1 ; i <= n ; i++) num[i] = std::lower_bound(b + 1 , b + tot + 1 , num[i]) - b ; 
	m = read() ; 
	for(qwq int i = 1 ; i <= m ; i++) 
	block[i].l = read() , block[i].r = read() , block[i].id = i ; 
	std::sort(block + 1 , block + m + 1 , cmp1) ; 
	for(qwq int i = 1 , id = 1 ; id <= Max ; id++) i = work(i , id) ;  
	std::sort(block + 1 , block + m + 1 , cmp2) ; 
	for(qwq int i = 1 ; i <= m ; i++) printf("%lld\n" , block[i].ans) ; 
	return 0 ;
}

题单:回滚莫队

AT1219 歴史の研究
P5386 [Cnoi2019]数字游戏
P6072 『MdOI R1』Path

写在最后

个人感觉莫队没有什么太大的说头,如有哪些地方不明白,还请直接让博主知道,予以补充,关于题解的话,都是洛谷上的题,题解就直接看那上面的吧,反正都是模板题(打的最后不一样就赖不着了),只讲一下流程应该就 \(OK\) 的。 本来不打算写代码,但是感觉太少了,就附上代码了。

posted @ 2021-02-06 10:24  SkyFairy  阅读(119)  评论(0编辑  收藏  举报