29-30 考试总结

来源:(COCI2011)~(2012) (test1)-(test2)部分题目。

T1 ples

题目大意

(N)个男生和(N)个女生参加舞会,跳舞时只能是一个男生一个女生,我们知道每个人的身高,同时某个男生只想和比他高(或者矮)的女生跳(有人跟你跳还挑三拣四的?),女生也是一样的,身高一样的人都不想与对方跳。给出所有数据,输出满足人们希望的前提下组成的最多对数。

分析

开始拿到题看了看,哦?二分图最大匹配板子?

直到看到了浩瀚无边的数据范围。。。正当心中默念"哦豁"时,看到了很小的值域范围。。。

人生大起大落真刺激

恩,带权二分图最大匹配,因为值域很小,我们可以把每个值看做点,人数看做点权,按题目要求连边,男生女生分别连向源点和汇点,然后就是跑最大流了。

因为内存限制,所以要精确算开多少条边。

代码

#include<queue>
#include<cstdio>
#include<cstdlib>
#include<cstring>
const int BASE = 1500; 
const int INF = 0x3f3f3f3f;
inline int read(){
	int f = 1, x = 0; char ch;
	do { ch = getchar(); if (ch == '-') f = -1; } while (ch < '0' || ch > '9');
	do {x = (x << 3) + (x << 1) + ch - '0'; ch = getchar(); } while (ch >= '0' && ch <= '9'); 
	return f * x;
}
inline void hand_in() {
	freopen("ples.in", "r", stdin);
	freopen("ples.out", "w", stdout);
} 
int n, s, t, maxflow;
struct Node{ int l, r; }A[1005], B[1005];
inline int min(int a, int b) { return a < b ? a : b; } 
struct Sakura { int to, nxt, w; }sak[2200005]; int head[4010], cnt = 1;
inline void add(int x, int y, int w) {
	++cnt;
	sak[cnt].to = y, sak[cnt].w = w, sak[cnt].nxt = head[x], head[x] = cnt;
	++cnt;
	sak[cnt].to = x, sak[cnt].w = 0, sak[cnt].nxt = head[y], head[y] = cnt;
//	printf("%d -> %d : %d
", x, y, w);
}

int dep[4010];
inline bool bfs() {
	std :: queue<int> q;
	memset(dep, 0, sizeof dep);
	dep[s] = 1, q.push(s);
	while(!q.empty()) {
		int u = q.front();
		q.pop();
		for (int i = head[u];i;i =sak[i].nxt) {
			int v = sak[i].to, w = sak[i].w;
			if (!dep[v] && w) {
				dep[v] = dep[u] + 1;	
				if (v == t) return 1;
				q.push(v);
			}
		}
	}
	return 0;
}

inline int dfs(int u, int flow) {
	if (u == t) return flow;
	int rest = flow, rlow;
	for (int i = head[u];i && rest;i = sak[i].nxt) {
		int v = sak[i].to, w = sak[i].w; 
		if (w && dep[v] == dep[u] + 1) {
			rlow = dfs(v, min(rest, w));
			if (!rlow) dep[v] = 0;
			sak[i].w -= rlow;
			sak[i ^ 1].w += rlow;
			rest -= rlow;
		}
	}
	return flow - rest;
}

inline int Dinic() {
	int lowflow;
	while (bfs()) {
		while(lowflow = dfs(s, INF)) maxflow += lowflow;
	}
	return maxflow;
}

int main(){
	hand_in();
	n = read();
	s = 4004, t = 4005;
	for (int i = 1, x;i <= n; ++i) {
		x = read();
		if (x < 0) A[- x - BASE].l ++;
		else A[x - BASE].r ++;
	}

	for (int i = 1, x;i <= n; ++i) {
		x = read();
		if (x < 0) B[- x - BASE].l ++;
		else B[x - BASE].r ++;
	}

	/* A.l 0~1000 A.r 1001~2001 B.l 2002~3002  B.r 3003~4003 */

	/* S --- A.l / A.r 连源点 */
	for (int i = 0;i <= 1000; ++i) {
		if (A[i].l) add(s, i, A[i].l);
		if (A[i].r) add(s, i + 1001, A[i].r);
	}

	/* A.l --- B.r 男高女矮 */
	for (int i = 1;i <= 1000; ++i) {
		for (int j = 0;j < i; ++j) {
			if (A[i].l && B[j].r) add(i, j + 3003, INF);
		}
	}

	/* A.r --- B.l 男矮女高 */
	for (int i = 0;i < 1000; ++i) {
		for (int j = i + 1;j <= 1000; ++j) {
			if (A[i].r && B[j].l) add(i + 1001, j + 2002, INF);
		}
	}

	/* B.l / B.r --- T 连汇点 */
	for (int j = 0;j <= 1000; ++j) {
		if (B[j].l) add(j + 2002, t, B[j].l);
		if (B[j].r) add(j + 3003, t, B[j].r);
	}

	printf("%d", Dinic());
	return 0;
}

排序+贪心是正解???

好吧,的确是的。

我们可以发现,排序后让正数与负数匹配,用指针一直向后移动,那么会让后面的情况达到最优。

T2 sort

题目大意

对一个序列使用(reverse)函数排成升序,每次(reverse)序列中所有连续单调下降子序列,保证在第一次排序后,所有的连续单调下降子序列的长度都是偶数。求(reverse)函数的使用次数。

分析

30pt做法

直接用(reverse)函数模拟,你甚至可以自己手写

100pt做法

首先,有个结论,在第一次对序列执行这种方法的排序后,序列中所有的连续单调下降子序列的长度要么是(3)要么是(2)(1)排除在外)。

证明:考虑相邻的两个连续单调下降子序列,有两种情况:它们中间没有间隔;它们中间有一个元素的间隔。对于第一种情况,(reverse)这两个子序列后,只会在它们俩的交界处形成一个长度为(2)的单调下降序列;对于第二种情况,同理,会形成一个长度为(3)的单调下降序列。

又因为题目保证都为偶数,所以在第一次排序后,所有的连续单调下降子序列的长度都是(2)

这有什么性质?在之后我们每次(reverse),就会让一个数的前面比它大的数减(1),又因为递增序列所有数都比它前面的数大,所以联想到求逆序对。

所以这道题的解法就是先模拟排序一遍该序列,累加答案,然后计算当前序列的逆序对数,累加进答案里面。

代码

#include<cstdio>
#include<cstdlib>
#include<algorithm>
#define ll long long
const int N = 100000 + 5;
using std :: reverse;
inline int read(){
	int f = 1, x = 0; char ch;
	do { ch = getchar(); if (ch == '-') f = -1; } while (ch < '0' || ch > '9');
	do {x = (x << 3) + (x << 1) + ch - '0'; ch = getchar(); } while (ch >= '0' && ch <= '9'); 
	return f * x;
}
inline void hand_in() {
	freopen("sort.in", "r", stdin);
	freopen("sort.out", "w", stdout);
} 
int n, a[N];
ll ans;
inline bool judge() {
	for (int i = 2;i <= n; ++i) {
		if (a[i] < a[i - 1]) return 1;
	}
	return 0;
}
int c[N];
inline int lowbit(int x) {
	return x & (-x);
}
inline void add(int x) {
	for (int i = x;i <= n; i += lowbit(i)) c[i] ++;
}
inline int ask(int x) {
	int res = 0;
	if (!x) return 0;
	for (int i = x;i;i -= lowbit(i)) res += c[i];
	return res;
}
inline void solve() {
	int st = 1, ed = 1, len = 1;
	for (int i = 2;i <= n; ++i) {
		if (a[i] < a[i - 1]) {
			ed = i, len ++;
		}
		else {
			if (len > 1) {
				reverse(a + st, a + ed + 1);		
				ans ++;		
			}
			st = i, ed = i, len = 1;
		}
	}
	if (len > 1) {
		reverse(a + st, a + ed + 1);		
		ans ++;
	}
	for (int i = n;i >= 1; --i) {
		ans += ask(a[i] - 1);
		add(a[i]);
	}
}
int main(){
	hand_in();
	n = read();
	for (int i = 1;i <= n; ++i) a[i] = read(); 
	solve();
	printf("%lld", ans);
	return 0;
}

T3 skakac(To be continue)

题目大意

略略略~

分析

20pt做法

也许直接(dfs)

30~50pt做法

(f[i][j][k])表示当前时间为(i),棋盘位置为((j,k))是否到达的状态,然后就可以枚举(i-1)的状态转移过来,用个滚动数组优化下,空间复杂度(O(n^2)),时间复杂度(O(Tn^2))

同时,也可以上(BFS)

60~100pt做法

鬼知道数据有多恐怖,貌似正确的复杂度只有(70pt)???

考虑压缩每一行的状态,然后就可以(O(Tn))转移。

难就难在处理图上的(K_{i,j})

为了保证时间复杂度和空间复杂度,对(K_{i,j})分开处理。

如果(K_{i,j} > 1000)那么把在(T)以内的(K_{i,j})的倍数记录下来,当(T)达到这个值的时候再处理。 如果(K_{i,j}le 1000),可以把(K_{i,j})分解质因数,设=(K_{i,j}=p_1^{c_1}p_2^{c_2}...p_k^{c_k}),那么当(T)能同时整除(_1^{c_1}p_2^{c_2}...p_k^{c_k}) 时当前((i,j))就可到达。
(g[i][j][k])表示第k行能整除(p_i^j)时的情况,那么把(T)也分解质因数,棋盘的情况就可以表示为(g[1][c_1] & g[2][c_2] ... g[k][c_k])
当然次数为(0)的也要考虑进去,为了节省时间可以令(h[i][j]=g[i][0]&g[i+1][0]...g[j][0])

然后就可以预处理出所有状态下图对应的状态,也许写得有点问题,数据点过不全。

代码

#include<vector>
#include<cstdio>
#include<cstdlib>
#include<cstring>
#include<algorithm>
#define ll long long
#define Re register
const int P = 1000000007;
const int BASE = 1e6;
const int N = 30 + 5;
using std :: vector;
using std :: sort;
inline void hand_in() {
	freopen("skakac.in", "r", stdin);
	freopen("skakac.out", "w", stdout);
}
inline int read(){
	int f = 1, x = 0; char ch;
	do { ch = getchar(); if (ch == '-') f = -1; } while (ch < '0' || ch > '9');
	do {x = (x << 3) + (x << 1) + ch - '0'; ch = getchar(); } while (ch >= '0' && ch <= '9'); 
	return f * x;
}
int n, t, st_x, st_y;
int mp[N][N];

struct Node {
	int t, x, y;
	Node (int a = 0, int b = 0, int c = 0) : t(a), x(b), y(c) {}
	friend bool operator < (Node a, Node b) { return a.t < b.t; }
}ans[N * N], rate[BASE + 5]; int oo, pp;

/* 预处理质数 */
int prim[BASE + 5], vis[BASE + 5], tot, ps;
inline void init() {
	for (Re int i = 2;i <= BASE; ++i) {
		if (!vis[i]) {
			prim[++tot] = i, vis[i] = tot;
			if (i <= 1000) ps = tot; /* 小优化:记录下1000以内的质数到了哪里 */
		}
		for (Re int j = 1;j <= tot; ++j) {
			if ((ll)i * (ll)prim[j] > BASE) break;
			vis[i * prim[j]] = j;
			if (i % prim[j] == 0) break;
		}
	}
}

/* 对(x,y)上的k值进行质因数分解 */
/*
h: h[i][j][k]表示 
g: g[i][j][k]表示第k行能整除pi^j时的情况 
*/
int h[250][250][35], g[250][25][35];
inline void divide(int k, int x, int y) {
	for (Re int i = 1;i <= ps; ++i) {
		int ret = 0;
		while (k % prim[i] == 0) ret ++, k /= prim[i];
		/* 既然x^ret能够到达,所以高于ret的次幂都行,最多达到2^20 */
		for (;ret <= 20; ++ret) g[i][ret][x] |= (1 << y);
	}
}

/* 质因数分解 */
struct Divide {
	int pr, nm;
	friend bool operator < (Divide a, Divide b) { return a.pr < b.pr; }
}dv[BASE + 5]; int w[35];
inline void work(int x) {
	int ct = 0, pos = 1;

	/* 会超时??? */
//	for (int i = 1, ret;prim[i] <= x; ++i) {
//		if (x % prim[i]) continue;
//		ret = 0;
//		while (x % prim[i] == 0) ret ++, x /= prim[i];
//		if (ret) dv[++ct].pr = i, dv[ct].nm = ret;
//	}

	while (x ^ 1) {
		int ret = 0, op = vis[x];
		while (x % prim[op] == 0) ret ++, x /= prim[op];
		dv[++ct].pr = op, dv[ct].nm = ret;		
	}
	sort(dv + 1, dv + 1 + ct);
	
	for (int i = 1;i <= ct && dv[i].pr <= ps; ++i) {
		for (int j = 1;j <= n; ++j) {
			w[j] &= g[dv[i].pr][dv[i].nm][j];
		}
		if (pos < dv[i].pr) {
			for (int j = 1;j <= n; ++j) {
				w[j] &= h[pos][dv[i].pr - 1][j];
			}
		}
		pos = dv[i].pr + 1;
	}
	if (pos < ps) for (int i = 1;i <= n; ++i) w[i] &= h[pos][ps][i]; 
}

int f[2][35], now, pre;
int main(){
	hand_in();

	/* 读入 */
	n = read(), t = read(), st_x = read(), st_y = read();

	/* 预处理出质数 */
	init();

	/* 对地图分类处理:1000以上用倍数,反之分解它 */
	for (Re int i = 1;i <= n; ++i) {
		for (Re int j = 1;j <= n; ++j) {
			mp[i][j] = read();
			if (mp[i][j] >= 1500) {
				for (Re int k = 1;k * mp[i][j] <= t; ++k) rate[++pp] = Node(k * mp[i][j], i, j);
			}
			else {
				divide(mp[i][j], i, j);
			}
		}
	}

	/* 排序后可以依次考虑 */
	sort(rate + 1, rate + 1 + pp);
	for (Re int i = 1;i <= ps; ++i) {
		for (Re int j = i;j <= ps; ++j) {
			for (Re int s = 1;s <= n; ++s) {
				/* 初始化全为1 */
				h[i][j][s] = (1 << (n + 1)) - 1;
				for (Re int k = i;k <= j; ++k) {
					h[i][j][s] &= g[k][0][s];
				}
			}
		}
	}
	f[0][st_x] |= (1 << st_y);
	for (Re int i = 1, p = 1;i <= t; ++i) {
		now ^= 1, pre = now ^ 1;
		for (Re int j = 1;j <= n; ++j) f[now][j] = 0, w[j] = (1 << (n + 1)) - 1;
		for (Re int j = 1;j <= n; ++j) {
			if (j > 1) f[now][j] |= (f[pre][j - 1] >> 2) | (f[pre][j - 1] << 2);
			if (j < n) f[now][j] |= (f[pre][j + 1] >> 2) | (f[pre][j + 1] << 2);
			if (j > 2) f[now][j] |= (f[pre][j - 2] >> 1) | (f[pre][j - 2] << 1);
			if (j < n - 1) f[now][j] |= (f[pre][j + 2] >> 1) | (f[pre][j + 2] << 1);
		}
		work(i);
		while (p <= pp && rate[p].t == i) w[rate[p].x] |= (1 << rate[p].y), ++p;
		for (Re int j = 1;j <= n; ++j) f[now][j] &= w[j];
	}
	for (Re int i = 1;i <= n; ++i) {
		for (Re int j = 1;j <= n; ++j) {
			if (f[now][i] & (1 << j)) ans[++oo].x = i, ans[oo].y = j;
		}
	}
	printf("%d
", oo);
	for (Re int i = 1;i <= oo; ++i) {
		printf("%d %d
", ans[i].x, ans[i].y);
	}
	return 0;
}

T4 kom

题目大意

给出(N)个互不相同的正整数,统计共有多少对数,它们有公共的一个数字(不一定在同一位置上)。

分析

20pt做法

无脑暴力。

60pt做法

假的容斥。

考试时最先想到这个方法。

对每个数用二进制状态存下来(O(n))扫描,然后对1024种状态容斥一下,就是(O(n imes 1024))

貌似这种方法用bitset优化下就能过?

100pt做法

我们不需要知道所有数,只需要知道某个数出现了哪些数字,思考压缩状态,即只用出现了哪些数字来表示一个数,又因为数字只有(0)~(9),所以整个状态总量就是(2^{10}=1024)。然后我们就可以枚举压缩后的状态啦,记得不能算自身,所以要减去自己与自己成对的情况。

代码

#include<cstdio>
#include<cstdlib>
#include<cstring>
#define ll long long
inline void hand_in() {
	freopen("kom.in", "r", stdin);
	freopen("kom.out", "w", stdout);
}
int n, len, v, pan[1025];
char ch[20]; ll ret;
int main(){
	hand_in();
	scanf("%d", &n), v = (1 << 10);
	for (int i = 1, a;i <= n; ++i) {
		scanf("%s", ch + 1);
		len = strlen(ch + 1), a = 0;
		for (int j = 1;j <= len; ++j) {
			a |= (1 << (ch[j] - '0'));
		}
		pan[a] ++;
	}
	for (int i = 0;i < v; ++i) {
		for (int j = 0;j < v; ++j) {
			if (i & j) {
				ret += (ll)pan[i] * (ll)pan[j];
				if (i == j) ret -= (ll)pan[i];
			}
		}
	}
	printf("%lld
", ret >> 1);
	return 0;
}

T5 fun

题目大意

略略略~

分析

30pt做法

直接按照那个函数用(dfs)模拟即可。

100pt做法

我们可以发现,若一层循环中上下界为常数项,我们可以把它换在任何位置,对答案没有任何影响,只需要在统计答案时乘上这层循环的循环次数即可。

推广一下,当我们确定一个字母变量的值的时候,那么所有依赖于它的变量循环都可变为常数循环,我们任意交换是没有问题的。根据依赖关系,我们会发现是由一棵棵树组成的森林,每棵树相互之间是不会受到影响的,所以直接把每棵树相乘即为最终答案。

对于每棵树,我们从上往下遍历,就能依次确定各个变量的取值范围,然后将子树的值相乘,再上传就是答案。

用记忆化优化下复杂度。

代码

#include<cstdio>
#include<cstdlib>
#include<cstring>
#define ll long long
const int P = 1000000007;
const int N = 30;
inline void hand_in() {
	freopen("fun.in", "r", stdin);
	freopen("fun.out", "w", stdout);
}
inline bool is_num(char *s) { if (s[1] < '0' || s[1] > '9') return 0; return 1; }
int cnt, to[N << 1], nxt[N << 1], head[N]; inline void add(int x, int y) { ++cnt; to[cnt] = y, nxt[cnt] = head[x], head[x] = cnt; }
inline int change_num(char *s) { int l = strlen(s + 1); int res = 0; for (int i = 1;i <= l; ++i) { res = res * 10 + s[i] - '0'; } return res; }

ll dfs(int, int);
ll f[N][100005], ans = 1;
int n, l[N], r[N], rate[N];
char ls[10], rs[10];

inline ll find(int now, int lim) {
	ll res = 1;
	int p = head[now], nt;
	while (p) {
		nt = to[p];
		res *= dfs(nt, lim) % P;
		p = nxt[p];
	}
	return res;
}

inline ll dfs(int now, int lim) {
	if (~f[now][lim]) return f[now][lim];
	int last = lim;
	if (rate[now] == 0) { 
		while (last <= r[now] && f[now][last] == -1) last ++;
		if (last > r[now]) f[now][last] = 0;
		last --;
		while (last >= lim) {
			f[now][last] = (f[now][last + 1] + find(now, last)) % P;
			last --;
		}
	}
	else if (rate[now] == 1) { 
		while (last >= l[now] && f[now][last] == -1) last --;
		if (last < l[now]) f[now][last] = 0;
		last ++;
		while (last <= lim) {
			f[now][last] = (f[now][last - 1] + find(now, last)) % P;
			last ++;
		}		
	}
	else { 
		f[now][lim] = 0;
		for (int i = l[now];i <= r[now]; ++i) {
			f[now][i] = find(now, i);
			f[now][lim] = (f[now][i] + f[now][lim]) % P;
		}
	}
	return f[now][lim];
}

int main(){
//	hand_in();
	scanf("%d", &n);
	memset(f, -1, sizeof f);
	for (int i = 0;i < n; ++i) {
		scanf("%s %s", ls + 1, rs + 1);
		rate[i] = -1;  
		if (is_num(ls)) {
			l[i] = change_num(ls);
		}
		else {
			rate[i] = 0; 
			l[i] = ls[1] - 'a';
			add(l[i], i);
		}

		if (is_num(rs)) {
			r[i] = change_num(rs);
		}
		else {
			rate[i] = 1; 
			r[i] = rs[1] - 'a';
			add(r[i], i);
		}
	}
	for (int i = 0;i < n; ++i) {
		if (rate[i] == -1) {
			ans = ans * dfs(i, 0) % P;
		}
	}
	printf("%lld", ans);
	return 0;
}

T6 ras

题目大意

略略略~

分析

30pt做法

把计算答案的式子列出来后会发现可以贪心。

即把序列按(t)的升序排序,然后直接计算答案。

对于每次修改,暴力修改,再重新排遍序,再计算答案。

100pt做法

思路大体没错,算法的瓶颈在于修改,也就是动态维护前缀和。

考虑使用数据结构权值线段树或权值树状数组。

可以把维护前缀和拆成两个操作,清除某位上的一个数,把一个数插入到某位上。

在操作的同时对(ans)进行维护。

注意需要开(ll)

代码

#include<cstdio>
#include<cstdlib>
#include<algorithm>
#define ll long long
using std :: sort;
const int N = 100000 + 5;
inline int read(){
	int f = 1, x = 0; char ch;
	do { ch = getchar(); if (ch == '-') f = -1; } while (ch < '0' || ch > '9');
	do {x = (x << 3) + (x << 1) + ch - '0'; ch = getchar(); } while (ch >= '0' && ch <= '9'); 
	return f * x;
}
inline void hand_in() {
	freopen("ras.in", "r", stdin);
	freopen("ras.out", "w", stdout);
}
int n, c; ll ans, s;
struct Data { int l, t; }mk[200005], rsd[200005];
inline bool cmp(const Data &a, const Data &b) { return a.t < b.t; }

struct Segment_Tree {
	struct Node {
		int l, r;
		ll x, t;
	}tr[N << 2];
	
	#define ls (p << 1)
	#define rs ((p << 1) | 1)
	
	inline void build(int p, int l, int r) {
		tr[p].l = l, tr[p].r = r;
		if (l == r) return;
		int mid = (l + r) >> 1;
		build(ls, l, mid), build(rs, mid + 1, r);
	}
	
	inline void change(int p, int x, int a, int b) {
		tr[p].x += a, tr[p].t += b;
		if (tr[p].l == tr[p].r) return;
		int mid = (tr[p].l + tr[p].r) >> 1;
		if (x <= mid) change(ls, x, a, b);
		else change(rs, x, a, b);
	}
	
	inline ll ask_x(int p, int l, int r) {
		if (l <= tr[p].l && tr[p].r <= r) {
			return tr[p].x;
		}
		int mid = (tr[p].l + tr[p].r) >> 1;
		ll res = 0;
		if (l <= mid) res += ask_x(ls, l, r);
		if (r > mid) res += ask_x(rs, l, r);
		return res;
	}
	
	inline ll ask_t(int p, int l, int r) {
		if (l <= tr[p].l && tr[p].r <= r) {
			return tr[p].t;
		}
		int mid = (tr[p].l + tr[p].r) >> 1;
		ll res = 0;
		if (l <= mid) res += ask_t(ls, l, r);
		if (r > mid) res += ask_t(rs, l, r);
		return res;
	}
}st;

int main(){
	hand_in();
	n = read(), c = read();
	for (int i = 1;i <= n; ++i) {
		mk[i].l = read(), mk[i].t = read();
		rsd[i] = mk[i], ans += mk[i].l;
	}
	st.build(1, 1, 100001);
	sort(mk + 1, mk + 1 + n, cmp);
	for (int i = 1;i <= n; ++i) {
		s += mk[i].t;
		ans -= s;
		st.change(1, mk[i].t, mk[i].t, 1);
	}
	printf("%lld
", ans);
	for (int i = 1, id, l, t;i <= c; ++i) {
		id = read(), l = read(), t = read();
		ans -= rsd[id].l - st.ask_x(1, 1, rsd[id].t) - st.ask_t(1, rsd[id].t + 1, 100001) * rsd[id].t;
		st.change(1, rsd[id].t, -rsd[id].t, -1);
		rsd[id].l = l, rsd[id].t = t;
		st.change(1, rsd[id].t, rsd[id].t, 1);
		ans += rsd[id].l - st.ask_x(1, 1, rsd[id].t) - st.ask_t(1, rsd[id].t + 1, 100001) * rsd[id].t;
		printf("%lld
", ans);
	}
	return 0;
}
原文地址:https://www.cnblogs.com/silentEAG/p/11731968.html