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,n−1]与 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 i−1的位置,把 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柱上无球:
- 设 x x x柱子上有 s s s关键球
- 将 y y y号柱上的 s s s个球移动到 z z z号柱上。
- 依次考虑
x
x
x号柱里的每一个球。
1. 1. 1. 若该球为关键球,则将其移动到 y y y号柱
2. 2. 2. 若该球不为关键球,则将其移动到 z z z号柱。 - 将 z z z号柱上方的 m − s m-s m−s个球移回 x x x号柱。
- 将 y y y号柱上方的 s s s个球1移动到 x x x号柱。
- 将 z z z号柱里面的 s s s个球移动到 y y y号柱。
- 将 x x x号柱上方的 s s s个球移动到 y y y号柱
- 依次考虑
y
y
y号柱里面的每一个球。
1. 1. 1. 若该球为关键球,则将其移动到 z z z号柱
2. 2. 2. 若该球不为关键球,则将其移动到 x x x号 - 此时 n = 2 n=2 n=2就做完了,复杂度为 O ( m ) O(m) O(m)。
那么我们考虑解决 n ≤ 50 n\le50 n≤50的情况。
我们考虑设计一个阈值,大于 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(rj−lj+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;
}