2020 NOIP题解

T1 P7113 [NOIP2020] 排水系统


原题链接

首先我们要知道这道题用到了高精度,看完得知这道题标签给到了拓扑排序,后来找了题解看了看,确实是一道拓扑排序的裸题。

但我们可以看一下数据范围: m m m最大为 10 10 10 n n n最大为 1 0 5 10^5 105

既然数据范围不是很大,我们考虑一下暴力做法。

我们可以依次向水管中注水,并进行排水操作。

只不过用这种方法就一定要记得一个点,就是你排水排完一轮后要重新再搜几次。

因为很可能 之后工作的水管的水流入之前工作过的水管了, 做到这一点,就会得到 90 p t s 90pts 90pts的好成绩。

90 p t s 90pts 90pts(我还以为是正解)

#include<bits/stdc++.h>
using namespace std;
#define int unsigned long long
int read() {
	int x = 0, f = 1;
	char ch = getchar();
	while (ch < '0' || ch > '9') {
		if (ch == '-') f = -1;
		ch = getchar();
	}
	while (ch >= '0' && ch <= '9')
		x = x * 10 + ch - '0', ch = getchar();
	return x * f;
}
const int N = 1e5 + 10;
int n, m;
int d[N];
int ls[N][6];
int mu[N], zi[N];//分子and分母
int ans[N];
void judge(int a, int b) {//通分操作
	int x = __gcd(mu[a], mu[b] * d[b]);
	int y = mu[a] / x * mu[b] * d[b];
	zi[a] = (y / mu[a]) * zi[a]; //分子同分母变化而变化(分母在最后改变☆*: .?. o(≧▽≦)o .?.:*☆
	zi[a] += y / mu[b] / d[b] * zi[b]; //相加
	mu[a] = y; //分母变成公分母了
}
void check(int i, int j) { //几个出水管 & 第几个排水管;
	//i为d[j]
	for (int k = 1; k <= i; k++) {
		int x = ls[j][k]; //排向第x个
		if (mu[x] == 0 && zi[x] == 0) {
			mu[x] = mu[j] * i;
			zi[x] = zi[j];
			int wc = __gcd(mu[x], zi[x]);
			mu[x] /= wc;
			zi[x] /= wc;
		} else  judge(x, j);
	}
	//排完注意清零
	mu[j] = 0;
	zi[j] = 0;
}
signed main() {
	n = read(), m = read();
	for (int i = 1; i <= n; i++) {
		d[i] = read();
		for (int j = 1; j <= d[i]; j++) {
			ls[i][j]=read();
		}
	}
	for (int i = 1; i <= m; i++) { //第几个接受口
		zi[i] = mu[i] = 1; //初始赋值
		for (int j = 1; j <= n; j++) { //第几个水管
			if (d[j] == 0 || (mu[j] == 0 && zi[j] == 0) ) continue;
			check(d[j], j);
		}
		//写完这个再扫几遍看有没有漏掉的(就是之后工作的水管的水流入之前工作过的水管了
		while (1) {
			bool flag = false;
			for (int j = 1; j <= n; j++) {
				if ( d[j] > 0 && mu[j] > 0 && zi[j] > 0 ) {
					check(d[j], j);
					flag = true;
				}
			}
			if (flag == false) break;
		}
	}
	for (int i = 1; i <= n; i++) {
		if (d[i] == 0) {
			int lucy = __gcd(mu[i], zi[i]);
			zi[i] = zi[i] / lucy;
			mu[i] = mu[i] / lucy;
			cout << zi[i] << " " << mu[i] << endl;
		}
	}
	return 0;
}
//完结撒花!!!(^_-)db(-_^)

既然这是拓扑排序的裸题,那就想想拓扑排序的做法。

首先我们需要了解什么是拓扑排序:

拓扑排序主要思路在一个有向无环图中,先统计出每个点的入度个数,然后将入度为0的点入队,接着把队中每个点向它的出边做一个运算(本题中是将水分流到与其相连节点),然后断边(相连的点入度-1),最后就会得出排水节点的水量。

下面是拓扑排序的模板:

void tp(){
	for(int i=1;i<=n;i++)//所有入度为0的点入队(1-m)
		if(!in[i]){
			book[i]=1;
			q.push(i);
			xx[i]=1,yy[i]=1;
		}
	while(!q.empty()){
		int p=q.front();
		q.pop();
		if(out[p])
			continue;
		for(int i=0;i<a[p].size();i++){
			add(a[p][i],xx[p],yy[p]*(1ll*a[p].size()));
			if(book[a[p][i]])
				continue;
			in[a[p][i]]--;
			if(in[a[p][i]]==0){
				book[a[p][i]]=1;
				q.push(a[p][i]);
			}
		}
	}
	return;
}

注意高精度!!!
c o d e code code

#include<bits/stdc++.h>
#define ll long long
using namespace std;
inline ll read() {
	ll x = 0, f = 1;
	char ch = getchar();
	while (ch < '0' || ch > '9') {
		if (ch == '-')f = -1;
		ch = getchar();
	}
	while (ch >= '0' && ch <= '9') {
		x = x * 10 + ch - '0';
		ch = getchar();
	}
	return x * f;
}
int n, m, in[100001], out[100001], book[100001];
ll xx[100001], yy[100001];
ll gcd(ll x, ll y) {
	if (y == 0)
		return x;
	return gcd(y, x % y);
}
void add(int u, ll x, ll y) {
	if (y == 0)
		return;
	if (yy[u] == 0) {
		xx[u] = x;
		yy[u] = y;
		return;
	}
	ll p1 = xx[u] * y + yy[u] * x;
	ll p2 = yy[u] * y;
	ll p3 = gcd(p1, p2);
	xx[u] = p1 / p3;
	yy[u] = p2 / p3;
	return;
}
vector<int> a[500001];
queue<int> q;
void tp() {
	for (int i = 1; i <= n; i++)
		if (!in[i]) {
			book[i] = 1;
			q.push(i);
			xx[i] = 1, yy[i] = 1;
		}
	while (!q.empty()) {
		int p = q.front();
		q.pop();
		if (out[p])
			continue;
		for (int i = 0; i < a[p].size(); i++) {
			add(a[p][i], xx[p], yy[p] * (1ll * a[p].size()));
			if (book[a[p][i]])
				continue;
			in[a[p][i]]--;
			if (in[a[p][i]] == 0) {
				book[a[p][i]] = 1;
				q.push(a[p][i]);
			}
		}
	}
	return;
}
int main() {
	n = read(), m = read();
	for (int i = 1; i <= n; i++) {
		int d = read();
		if (d == 0) {
			out[i] = 1;
			continue;
		}
		while (d--) {
			int v;
			v = read();
			a[i].push_back(v);
			in[v]++;
		}
	}
	tp();
	for (int i = 1; i <= n; i++) {
		if (out[i]) {
			add(i, 0, 1);
			printf("%lld %lld\n", xx[i], yy[i]);
		}
	}
	return 0;
}

T2 P7114 [NOIP2020] 字符串匹配


原题链接

一个字符串哈希问题。

先来说一种简单的暴力做法:

枚举哪一段是 C C C,然后 h a s h hash hash判前面循环节(60pts)

//哈希
#include<bits/stdc++.h>
using namespace std;
#define int long long
int read() {
	int x = 0, f = 1;
	char ch = getchar();
	while (ch < '0' || ch > '9') {
		if (ch == '-') f = -1;
		ch = getchar();
	}
	while (ch >= '0' && ch <= '9')
		x = x * 10 + ch - '0', ch = getchar();
	return x * f;
}
const int N = 1100010;
int t, n;
int c[26];//枚举哪一段是c
int cnt;
int sum[N][27];
int ans;
string s;
const int base = 130;
int h[N], power[N];
int find(int l, int r) {
	return h[r] - h[l - 1] * power[r - l + 1];
}
void pow() {
	for (int i = 1; i <= n; i++) {
		h[i] = h[i - 1] * base + s[i];
	}
	for (int i = 1; i <= n; i++) {
		int x = s[i] - 'a';
		c[x]++;
		if (c[x] % 2)cnt++;//判断出现次数是否为偶数
		else cnt--;
		for (int j = 0; j <= 26; j++){
			sum[i][j] = sum[i - 1][j] + (cnt == j);
		}
	}
	for (int i = 1; i <= n; i++)
		for (int j = 1; j <= 26; j++)
			sum[i][j] += sum[i][j - 1];
}
bool check(int x, int len) {
	return find(1, x - len) == find(len + 1, x);
}
signed main() {
	t = read();
	power[0] = 1;
	for (int i = 1; i <= 1100000; i++) {
		power[i] = power[i - 1] * base;
	}
	while (t--) {
		memset(c, 0, sizeof(c));
		memset(sum, 0, sizeof(sum));
		cnt = 0;
		cin >> s;
		n = s.size();
		s = ' ' + s;
		pow();
		memset(c, 0, sizeof(c));
		cnt = 0, ans = 0;
		for (int i = n; i >= 3; i--) {
			int x = s[i] - 'a';
			c[x]++;
			if (c[x] % 2)cnt++;
			else cnt--;
			for (int len = 1; len * len < i; len++) {
				if ((i - 1) % len == 0) {
					if (check(i - 1, len)) {
						ans += sum[len - 1][cnt];
					}
					if (len * len < i - 1 && check(i - 1, (i - 1) / len)) {
						ans += sum[(i - 1) / len - 1][cnt];
					}
				}
			}
		}
		cout << ans << endl;
	}
	return 0;
}

接下来想想正解。
这个题可以用树状数组+KMP算法来实现。

我们可以来设 s [ i , j ] s[i,j] s[i,j]表示 s s s中从下标 i i i的位置到下标 j j j位置的子串,包括 i i i j j j。那么 z [ i ] z[i] z[i]表示 s [ i , n − 1 ] s[i,n-1] s[i,n1] s s s本身的最长公共前缀的长度。

此时KMP算法可以在 O ( n ) O(n) O(n)的时间内求出 z z z数组的所有位置。

我们定义 f ( i , j ) f(i,j) f(i,j)表示 s [ i , j ] s[i,j] s[i,j]中出现奇数次的字符的个数。

假设现在枚举到循环节长度为 i i i,此时根据前面的结论,我们算出来 k k k的方案数有 t t t种。其中一半 k k k是奇数,一半 k k k是偶数。

在从左往右扫字符串 s s s的过程中,假设我们目前枚举到位置 i i i,此时我们计算循环节长度为 i + 1 i+1 i+1的情况,此时 A A A最远可以取到 i − 1 i-1 i1的位置,把 i i i的位置留着给 B B B,保证 B B B不为空。我们可以用一个桶维护 i + 1 i+1 i+1开头的后缀里面每个字母出现的次数,当 i i i向右循环的时候,每次只改变一个字符,所以在桶的对应位置减 1 1 1,然后看看出现奇数次的字符数量如何变化就行了。

那么循环节里面的每个前缀中出现奇数次的字符个数,可以放到一个树状数组里面,这样这个树状数组一共有 26 26 26个位置,每个位置 p p p保存出现奇数次的字符有 p p p个的前缀有多少个。 i i i每向右循环 1 1 1位,就在计算完结果以后,把当前前缀扔到树状数组里面。

c o d e code code

#include<bits/stdc++.h>
using namespace std;
const int MAXN = (1 << 20) + 5;
char s[MAXN];//输入的字符串
int n, z[MAXN];//字符串长度,以及z函数的值
int before[30], after[30];//两个桶,分别统计当前枚举到的位置左侧和右侧每个字符出现的频次
int pre, suf, all;//当前枚举到的位置的前缀、后缀、整个串里面出现奇数次的字符的个数
//树状数组,对于s的某个前缀si,如果它里面出现奇数次的字符有m个,则在树状数组m+1位置+1
int c[30];
//lowbit
inline int lbt(int x) {
	return x & -x;
}
void update(int x) {
	while (x <= 27) {
		c[x]++;
		x += lbt(x);
	}
}
int sum(int x) {
	int r = 0;
	while (x > 0) {
		r += c[x];
		x -= lbt(x);
	}
	return r;
}
//扩展KMP算法,计算z函数的值
void Z() {
	z[0] = n;
	int now = 0;
	while (now + 1 < n && s[now] == s[now + 1]) {
		now++;
	}
	z[1] = now;
	int p0 = 1;
	for (int i = 2; i < n; ++i) {
		if (i + z[i - p0] < p0 + z[p0]) {
			z[i] = z[i - p0];
		} else {
			now = p0 + z[p0] - i;
			now = max(now, 0);
			while (now + i < n && s[now] == s[now + i]) {
				now++;
			}
			z[i] = now;
			p0 = i;
		}
	}
}
int main() {
	int T;
	cin >> T;
	while (T--) {
		cin >> s;
		n = strlen(s);
		memset(before, 0, sizeof(before));
		memset(after, 0, sizeof(after));
		memset(c, 0, sizeof(c));
		all = pre = suf = 0;
		Z();
		//如果发现循环节可以到结尾,减1,空至少一个位置给C
		for (int i = 0; i < n; ++i) {
			if (i + z[i] == n) z[i]--;
		}
		//先把字符串过一遍,频次统计到after数组里面
		for (int i = 0; i < n; ++i) {
			after[s[i] - 'a']++;
		}
		//扫一下每个字母,计算整个串中出现奇数次的字符的个数
		for (int i = 0; i < 26; ++i) {
			if (after[i] & 1) {
				all++;
			}
		}
		suf = all;//后缀中的值暂时和整个串一致
		long long ans = 0;
		for (int i = 0; i < n; ++i) {
			//再扫一次字符串,当循环到i位置的时候,循环节长度是i+1
			//s[i]要从右边去掉,维护after数组和suf变量
			if (after[s[i] - 'a'] & 1) {
				//之前是奇数,现在变成偶数了
				suf--;
			} else {
				suf++;
			}
			after[s[i] - 'a']--;
			//s[i]加到左边,维护before和pre变量
			if (before[s[i] - 'a'] & 1) {
				pre--;
			} else {
				pre++;
			}
			before[s[i] - 'a']++;
			if (i != 0 && i != n - 1) {
				//循环节大于1,才能对答案有贡献,因为题中说ABC都不为空
				int t = z[i + 1] / (i + 1) + 1;
				ans += 1LL * (t / 2) * sum(all + 1) + 1LL * (t - t / 2) * sum(suf + 1);
			}
			update(pre + 1);
		}
		cout << ans << endl;
	}
	return 0;
}

T3 P7115 [NOIP2020] 移球游戏


原题链接

这道题有点复杂,给定一 n + 1 n+1 n+1个最长为 m m m的栈,每个栈有 m m m*n 个球,共有 个球,共有 个球,共有m$种颜色,要构造1一种方案,使得在 820000 820000 820000步内把同一颜色的球都放在同一个栈里。

首先我们考虑 n = 2 n=2 n=2的过程。

我们考虑如果只有一个柱子有球,我们将这个柱子称为 x x x

然后,还有另外两个柱子没球,称为 y y y z z z。那么将 x x x上的球按照颜色分到 y y y z z z上,这个操作我们称之为分流。

额外的,我们将一个柱子上数量最少的球称为关键球。

n = 2 n=2 n=2的做法如下:

x x x y y y柱上有球, z z z柱上无球:

  1. x x x柱子上有 s s s关键球
  2. y y y号柱上的 s s s个球移动到 z z z号柱上。
  3. 依次考虑 x x x号柱里的每一个球。
    1. 1. 1. 若该球为关键球,则将其移动到 y y y号柱
    2. 2. 2. 若该球不为关键球,则将其移动到 z z z号柱。
  4. z z z号柱上方的 m − s m-s ms个球移回 x x x号柱。
  5. y y y号柱上方的 s s s个球1移动到 x x x号柱。
  6. z z z号柱里面的 s s s个球移动到 y y y号柱。
  7. x x x号柱上方的 s s s个球移动到 y y y号柱
  8. 依次考虑 y y y号柱里面的每一个球。
    1. 1. 1. 若该球为关键球,则将其移动到 z z z号柱
    2. 2. 2. 若该球不为关键球,则将其移动到 x x x
  9. 此时 n = 2 n=2 n=2就做完了,复杂度为 O ( m ) O(m) O(m)

那么我们考虑解决 n ≤ 50 n\le50 n50的情况。

我们考虑设计一个阈值,大于 v a l val val的当成 1 1 1,小于等于 v a l val val的当成 0 0 0,每次像冒泡排序一样不断分离每个柱子上的 0 0 0 1 1 1

我们发现阈值是可以分支的!

c o d e code code

#include<bits/stdc++.h>
using namespace std;
#define int long long
#define fo(i,a,b) for(int i=a;i<=b;i++)
#define rep(i,a,b) for(int i=a;i>=b;i--)
int read() {
	int x = 0, f = 1;
	char ch = getchar();
	while (ch < '0' || ch > '9') {
		if (ch == '-') f = -1;
		ch = getchar();
	}
	while (ch >= '0' && ch <= '9')
		x = x * 10 + ch - '0', ch = getchar();
	return x * f;
}
void write(int x) {
	if (x < 0) putchar('-'), x = -x;
	if (x > 9) write(x / 10);
	putchar(x % 10 + '0');
}
const int N = 60, M = 410, K = 820010;
int n, m;
int stk[N][M], top[N];
int cnt;
pair<int, int> ans[K];
bool st[N];
void work(int x, int y) {
	ans[++cnt] = {x, y};
	stk[y][++top[y]] = stk[x][top[x]--];
}
void solve(int l, int r) {
	if (l == r) return;
	int mid = (l + r) / 2;
	memset(st,0,sizeof(st));
	fo(i, l, mid) fo(j, mid + 1, r) {
		if (st[i] || st[j]) continue;
		int s = 0;
		fo(k, 1, m) s += (stk[i][k] <= mid);
		fo(k, 1, m) s += (stk[j][k] <= mid);
		if (s >= m) {
			s = 0;
			fo(k, 1, m) s += (stk[i][k] <= mid);
			fo(k, 1, s) work(j, n + 1);
			while (top[i]) {
				if (stk[i][top[i]] <= mid) work(i, j);
				else work(i, n + 1);
			}
			fo(k, 1, s) work(j, i);
			fo(k, 1, m - s) work(n + 1, i);
			fo(k, 1, m - s) work(j, n + 1);
			fo(k, 1, m - s) work(i, j);
			while (top[n + 1]) {
				if (top[i] == m || stk[n + 1][top[n + 1]] > mid) work(n + 1, j);
				else work(n + 1, i);
			}
			st[i] = 1;
		} else {
			s = 0;
			fo(k, 1, m) s += (stk[j][k] > mid);
			fo(k, 1, s) work(i, n + 1);
			while (top[j]) {
				if (stk[j][top[j]] > mid) work(j, i);
				else work(j, n + 1);
			}
			fo(k, 1, s) work(i, j);
			fo(k, 1, m - s) work(n + 1, j);
			fo(k, 1, m - s) work(i, n + 1);
			fo(k, 1, m - s) work(j, i);
			while (top[n + 1]) {
				if (top[j] == m || stk[n + 1][top[n + 1]] <= mid) work(n + 1, i);
				else work(n + 1, j);
			}
			st[j] = 1;
		}
	}
	solve(l, mid), solve(mid + 1, r);
}
signed main() {
	n =read(), m = read();
	fo(i, 1, n) fo(j, 1, m) stk[i][++top[i]] = read();
	solve(1, n);
	write(cnt), puts("");
	fo(i, 1, cnt) {
		write(ans[i].first);
		putchar(' ');
		write(ans[i].second);
		puts("");
	}
	return 0;
}

T4 P7116 [NOIP2020] 微信步数


原题链接

这道题看了0.6坤时也还是没看懂,只是会暴力做法。

我们称 n n n步为一轮,首先 − 1 -1 1的情况很好判断:一轮后回到原地且在第一轮里存在某个起点走不出去。

我们把要求的答案转换一下:原本是考虑每个起点各自走多少步出界,现在转换成同时考虑所有起点,把每天还 存活的起点 数量计入贡献。(这里存活就是指从该起点出发到某天还没出界)

一共有 m m m个维度,每个维度存活的位置是独立的,并且应是一段区间(只有开头、结尾的一部分会死亡)。

如果 j j j维存活的区间是 [ l j , r j ] [l_j,r_j] [lj,rj],那么总共存活的数量就为 ∏ j = 1 m ( r j − l j + 1 ) \prod_{j=1}^{m} (r_j-l_j+1) j=1m(rjlj+1)

45 p t s 45pts 45pts

int w[20], e[20], l[20], r[20];
int c[N], d[N];
int n, m;

int main() {
    scanf("%d%d", &n, &m);
    LL ans = 1;
    for (int i = 1; i <= m; i++) {
        scanf("%d", &w[i]);
        ans = ans * w[i] % mod;
    }
    for (int i = 1; i <= n; i++) {
        scanf("%d%d", &c[i], &d[i]);
    }
    while (1) {
        for (int i = 1; i <= n; i++) {
            e[c[i]] += d[i];
            l[c[i]] = min(l[c[i]], e[c[i]]);
            r[c[i]] = max(r[c[i]], e[c[i]]);
            LL s = 1;
            for (int j = 1; j <= m; j++) {
                if (r[j] - l[j] >= w[j]) goto M1;
               s = s * (w[j] - r[j] + l[j]) % mod;
            }
            ans = (ans + s) % mod;
        }
        bool lose = 1;
        for (int j = 1; j <= m; j++) {
            if (e[j] != 0) lose = 0;
        }
        if (lose) {
            ans = -1;
            break;
        }
    }
M1:
    printf("%lld\n", ans);
    return 0;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值