OI笔记 | 2022.11 做题记录(三)
还有不到一周 NOIP,别想太多,好好复习,然后 rp++。
- Z
- 低价购买
- [SCOI2010] 序列操作
- [USACO10MAR] Great Cow Gathering G
- Fibonacci
- K皇后
- MatryoshkaDoll
- [JRKSJ R5] 1-1 B
- [NOIP2020] 排水系统
- Story
Z
给定正整数 n,你要构造两个正整数 x,y,满足:
1≤x,y≤1018 x−y=n ω(x)=ω(y)其中 ω(m) 表示能整除 m 的素数个数,比如 ω(12)=2,因为 2∣12 且 3∣12。再比如 ω(998244353)=1,因为 998244353 是质数。
数据范围保证存在至少一组满足条件的 x,y。
T≤104,n≤109
题解
对于偶数 n,构造 x=2n,y=n 即可。显然 x 和 y 的质因数个数相同。
对于奇数 n,构造 x=pn,y=(p−1)n 即可,其中 p 为满足 p∤n 且 p≠2 的最小质数。这会让:
ω(pn) 比 ω(n) 多 1,即多了 p 这个质数。
ω[(p−1)n] 的也比 ω(n) 多 1,即由于 (p−1) 一定为偶数,所以多了 2 这个质数。
参考代码:
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const int MAX_N = 1e6;
vector<ll> primes;
bool vis[MAX_N];
inline ll read() {...}
inline void write(ll x) {...}
inline void init() {
for(int i = 2; i < MAX_N; i++) {
if(!vis[i]) primes.push_back(i);
for(int k = 0; k < primes.size() && primes[k] * i < MAX_N; k++) {
vis[primes[k] * i] = 1;
if(i % primes[k] == 0) break;
}
}
}
int main() {
init();
ll T = read();
while(T--) {
ll n = read();
if(n % 2 == 0) {
write(2 * n), putchar(' ');
write(n), putchar('\n');
}
else {
ll p = 1;
for(int i = 1; i < primes.size(); i++) {
if(n % primes[i] != 0) {
p = primes[i];
break;
}
}
write(p * n), putchar(' ');
write((p - 1) * n), putchar('\n');
}
}
return 0;
}
低价购买
求最长下降子序列的长度,以及达到该长度的最长下降子序列数。
题解
见注释。参考代码:
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const int MAX_N = 5e3 + 10;
int dp[MAX_N], a[MAX_N], l[MAX_N];
inline ll read() {...}
inline void write(ll x) {...}
int main() {
int n = read(), ans = 0, t = 0;
for(int i = 1; i <= n; i++) {
a[i] = read(), dp[i] = 1;
for(int j = 1; j < i; j++) if(a[i] < a[j]) dp[i] = max(dp[i], dp[j] + 1);
ans = max(ans, dp[i]);
if(dp[i] == 1) l[i] = 1;
for(int j = 1; j < i; j++) {
if(dp[i] == dp[j] + 1 && a[i] < a[j]) l[i] += l[j]; // 如果 dp 时是由 j 接上的,那么方案数从 j 累加过来
else if(dp[i] == dp[j] && a[i] == a[j]) l[j] = 0; // 如果 i 和 j 结尾的序列长度相同,值也相同,那么是相同的序列,会被重复计算,所以将 l[j] 和 l[i] 中的随意一个变成 0 即可
}
}
for(int i = 1; i <= n; i++) if(dp[i] == ans) t += l[i]; // l[i] 是方案数
write(ans), putchar(' '), write(t);
return 0;
}
[SCOI2010] 序列操作
给定一个 01 序列,序列里面包含了 n 个数,下标从 0 开始。这些数要么是 0,要么是 1,现在对于这个序列有五种变换操作和询问操作:
-
0 l r
把 [l,r] 区间内的所有数全变成 0 -
1 l r
把 [l,r] 区间内的所有数全变成 1 -
2 l r
把 [l,r] 区间内的所有数全部取反,也就是说把所有的 0 变成 1,把所有的 1 变成 0 -
3 l r
询问 [l,r] 区间内总共有多少个 1 -
4 l r
询问 [l,r] 区间内最多有多少个连续的 1
1≤n,m≤105。
题解
有点复杂的线段树,不太会写,于是照着小粉兔的题解写,不过码风是自己的码风。
以后多练练这种需要储存很多信息的线段树。
参考代码:
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const int MAX_N = 1e5 + 10;
bool a[MAX_N];
int tag1[MAX_N << 2], tag2[MAX_N << 2], len[MAX_N << 2];
struct node {
int T, F, Tlmax, Flmax, Trmax, Frmax, Tmax, Fmax;
} d[MAX_N << 2];
inline ll read() {...}
inline void write(ll x) {...}
inline int lc(int p) {return (p << 1);}
inline int rc(int p) {return (p << 1) | 1;}
inline int mid(int s, int t) {return s + ((t - s) >> 1);}
inline node U(node x, node y) {
node ret;
ret.T = x.T + y.T, ret.F = x.F + y.F;
if(x.F) ret.Tlmax = x.Tlmax;
else ret.Tlmax = x.T + y.Tlmax;
if(x.T) ret.Flmax = x.Flmax;
else ret.Flmax = x.F + y.Flmax;
if(y.F) ret.Trmax = y.Trmax;
else ret.Trmax = y.T + x.Trmax;
if(y.T) ret.Frmax = y.Frmax;
else ret.Frmax = y.F + x.Frmax;
ret.Tmax = max(max(x.Tmax, y.Tmax), x.Trmax + y.Tlmax);
ret.Fmax = max(max(x.Fmax, y.Fmax), x.Frmax + y.Flmax);
return ret;
}
inline void change(int p, int type) {
if(type == 0) {
tag2[p] = tag1[p] = 0;
d[p].T = d[p].Tlmax = d[p].Trmax = d[p].Tmax = 0;
d[p].F = d[p].Flmax = d[p].Frmax = d[p].Fmax = len[p];
}
else if(type == 1) {
tag2[p] = 0, tag1[p] = 1;
d[p].T = d[p].Tlmax = d[p].Trmax = d[p].Tmax = len[p];
d[p].F = d[p].Flmax = d[p].Frmax = d[p].Fmax = 0;
}
else if(type == 2) {
tag2[p] ^= 1;
swap(d[p].T, d[p].F);
swap(d[p].Tlmax, d[p].Flmax);
swap(d[p].Trmax, d[p].Frmax);
swap(d[p].Tmax, d[p].Fmax);
}
}
inline void pu(int p) {d[p] = U(d[lc(p)], d[rc(p)]);}
inline void pd(int p) {
if(tag1[p] != -1) change(lc(p), tag1[p]), change(rc(p), tag1[p]);
if(tag2[p]) change(lc(p), 2), change(rc(p), 2);
tag1[p] = -1, tag2[p] = 0;
}
void build_tree(int s, int t, int p) {
len[p] = t - s + 1, tag1[p] = -1;
if(s == t) {
d[p].T = d[p].Tlmax = d[p].Trmax = d[p].Tmax = a[s];
d[p].F = d[p].Flmax = d[p].Frmax = d[p].Fmax = (a[s] ^ 1);
return;
}
int m = mid(s, t);
build_tree(s, m, lc(p));
build_tree(m + 1, t, rc(p));
pu(p);
}
void update(int s, int t, int l, int r, int p, int type) {
if(r < s || l > t) return;
if(l <= s && t <= r) {change(p, type); return;}
pd(p);
int m = mid(s, t);
update(s, m, l, r, lc(p), type);
update(m + 1, t, l, r, rc(p), type);
pu(p);
}
node query(int s, int t, int l, int r, int p) {
if(r < s || l > t) return (node){0, 0, 0, 0, 0, 0, 0, 0};
if(l <= s && t <= r) return d[p];
pd(p);
int m = mid(s, t);
return U(query(s, m, l, r, lc(p)), query(m + 1, t, l, r, rc(p)));
}
int main() {
int n = read(), m = read();
for(int i = 1; i <= n; i++) a[i] = read();
build_tree(1, n, 1);
while(m--) {
int op = read(), l = read() + 1, r = read() + 1;
if(op < 3) update(1, n, l, r, 1, op);
else {
node ret = query(1, n, l, r, 1);
if(op == 3) write(ret.T);
else write(ret.Tmax);
putchar('\n');
}
}
return 0;
}
[USACO10MAR] Great Cow Gathering G
给出一个既有点权又有边权的树,求出它的重心到所有点的距离之和。
题解
洛谷 P1364 这题是有点权无边权的相同题目;洛谷 P1395 这题是无点权无边权的相同题目。其实做法都差不多:它们都有一个 O(n) 的 dp 做法。
不妨钦定 1 为树的根。我们用 ci 表示 i 的点权, depi 表示从 1 到 i 的距离, sizi 表示以 i 为根的子树的大小。另外,用 fi 表示 i 到所有点的距离之和,所以答案为 nmini=1fi。
显然我们 dfs 一遍就能求出所有的 depi 与 sizi。从而我们知道 f1=n∑i=1(depi⋅ci)。
考虑边 (u,v,w),显然 fv 可以由 fu 转移过来。具体来说,不在 v 子树里的 (siz1−sizv) 大小的点到 v 的距离都增加了 w;在 v 子树里的 sizv 大小的点到 v 的距离都减少了 w。合并一下,所以转移方程为:
fv=fu+siz1⋅w−2⋅sizv⋅w参考代码:
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const int MAX_N = 1e5 + 10;
ll c[MAX_N], siz[MAX_N], dep[MAX_N], f[MAX_N], dp[MAX_N], ans;
struct edge {int u, v, w;};
vector<edge> edges;
vector<int> G[MAX_N];
inline ll read() {...}
inline void write(ll x) {...}
inline void AddEdge(int u, int v, int w) {
edges.push_back({u, v, w});
G[u].push_back(edges.size() - 1);
}
void dfs1(int u, int fa) {
siz[u] = c[u];
for(int i = 0; i < G[u].size(); i++) {
int v = edges[G[u][i]].v, w = edges[G[u][i]].w;
if(v == fa) continue;
dep[v] = dep[u] + w;
dfs1(v, u);
siz[u] += siz[v];
}
}
void dfs2(int u, int fa) {
for(int i = 0; i < G[u].size(); i++) {
int v = edges[G[u][i]].v, w = edges[G[u][i]].w;
if(v == fa) continue;
f[v] = f[u] + siz[1] * w - 2 * siz[v] * w;
ans = min(ans, f[v]);
dfs2(v, u);
}
}
int main() {
int n = read();
for(int i = 1; i <= n; i++) c[i] = read();
for(int i = 1; i < n; i++) {
int u = read(), v = read(), w = read();
AddEdge(u, v, w), AddEdge(v, u, w);
}
dfs1(1, 0);
for(int i = 1; i <= n; i++) f[1] += dep[i] * c[i];
ans = f[1];
dfs2(1, 0);
write(ans);
return 0;
}
Fibonacci
给定长为 n 的序列 c,求它的最长斐波那契子序列的长度。
定义斐波那契序列为满足 ∀ 3≤i≤t,ai=ai−1+ai−2 的序列,其中 t 为序列的长度。
题解
用 dpm,n 表示以 am,an 为第 1 项和第 2 项的最长斐波那契子序列的长度。考虑从后向前进行转移,则转移方程为:
dpj,i=dpi,k+1转移的条件为 ak=aj+ai 且 1≤j<i<k≤n。
考虑用 hash 表来快速找到满足条件的 k。同时由于倒序枚举,可以在枚举到 i 的时候再赋值 idxai=i,这样 k 自然会满足 k>i。
参考代码:
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const int MAX_N = 3010;
int a[MAX_N], dp[MAX_N][MAX_N], ans;
unordered_map<int, int> idx;
inline ll read() {...}
inline void write(ll x) {...}
int main() {
int n = read();
for(int i = 1; i <= n; i++) a[i] = read();
for(int i = n; i > 1; i--) {
for(int j = 1; j < i; j++) {
if(idx.count(a[i] + a[j])) {
int k = idx[a[i] + a[j]];
dp[j][i] = dp[i][k] + 1;
ans = max(ans, dp[j][i] + 2);
}
}
idx[a[i]] = i;
}
write(ans);
return 0;
}
K皇后
小 Z 最近捡到了一个棋盘,他想在棋盘上摆放 K 个皇后。他想知道在他摆完这 K 个皇后之后,棋盘上还有多少个格子是不会被攻击到的。
注意:一个皇后会攻击到这个皇后所在的那一行,那一列,以及两条对角线。
1≤n,m≤2×104,1≤K≤500。
题解
枚举行,然后判断这个行能放多少皇后。
时间复杂度:O(n(m+k))。其中乘上 m 是因为要清空 f 标记数组与统计答案。
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
typedef pair<int, int> pii;
const int MAX_N = 2e4 + 10;
pii q[MAX_N];
bool vis[MAX_N], f[MAX_N];
inline ll read() {...}
inline void write(ll x) {...}
int main() {
int n = read(), m = read(), k = read(), ans = 0;
for(int i = 1; i <= k; i++) {
q[i].first = read();
q[i].second = read();
vis[q[i].first] = 1;
}
for(int i = 1; i <= n; i++) {
if(vis[i]) continue;
for(int j = 1; j <= m; j++) f[j] = 0;
for(int j = 1; j <= k; j++) {
int x = q[j].first, y = q[j].second;
f[y] = 1;
int ty1 = y - abs(x - i), ty2 = y + abs(x - i);
if(ty1 >= 1) f[ty1] = 1;
if(ty2 <= m) f[ty2] = 1;
}
for(int j = 1; j <= m; j++) ans += (f[j] == 0);
}
write(ans);
return 0;
}
MatryoshkaDoll
给出 n 个套娃的大小 a1,a2,…,an。给定 k,你要把这 n 个套娃分成 k 组,使得每一组都能嵌套。求出方案数 (mod 998244353) 。
一个组 c1,c2,…ct 嵌套指的是 ∀ 1≤i<t, aci+r≤aci+1,其中 r 是给定的常数。
多组数据,1≤T≤20,∑n≤50000。对于每组数据,有 1≤k≤n≤5000,1≤a1≤⋯≤an≤109,1≤r≤109。
题解
考虑 dp,类比子集划分(第二类斯特林数),我们用 dpi,j 表示把前 i 个数分成 j 组的方案数。如果不考虑能否嵌套,转移方程即为:
dpi,j=dpi−1,j−1+j⋅dpi−1,j边界条件为 dpi,i=1。
这是因为我们既可以把第 i 个单独分成新的一组( dpi−1,j−1 ),也可以把它分到原来 j 组的任意一个去( j⋅dpi−1,j ),然后根据加法原理,dpi,j 就是这两种情况方案数之和。
对于这题,唯一的区别就是不能分到前 j 组的任意一个去,要减掉不满足嵌套条件的组数。如果暴力枚举当前状态有多少不满足的组数,时间复杂度是 O(n3T) 的,但我们需要 O(n2T)。
所以考虑预处理。用 fi 表示前 i−1 个元素组成的组中不满足嵌套条件的组数,容易发现不论分成几组,fi 是唯一的,它等于 [1,i−1] 中满足 aj+r>ai 的 j 的个数。证明:考虑 a 的递增性质,如果 aj+r>ai,它必然是所在组别的最大值(因为满足条件的 j 不可能分在同一组),也就必然导致这一组不能与 i 嵌套。
于是转移方程为:
dpi,j=dpi−1,j−1+(j−fi)⋅dpi−1,j参考代码:
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const ll MAX_N = 5e3 + 10, MOD = 998244353;
ll dp[MAX_N][MAX_N], a[MAX_N], f[MAX_N];
inline ll read() {...}
inline void write(ll x) {...}
int main() {
int T = read();
while(T--) {
ll n = read(), k = read(), r = read();
for(ll i = 1; i <= n; i++) {
a[i] = read();
dp[i][i] = 1, f[i] = 0;
for(ll j = 1; j < i; j++) f[i] += (a[j] + r > a[i]);
}
for(ll i = 1; i <= n; i++) {
for(ll j = 1; j < i; j++) {
dp[i][j] = dp[i - 1][j - 1] + dp[i - 1][j] * (j - f[i]);
dp[i][j] %= MOD;
}
}
write(dp[n][k]), putchar('\n');
}
return 0;
}
[JRKSJ R5] 1-1 B
给出一个序列 a,∀i∈[1,n],ai∈1,−1。
询问有多少个将 a 重排后的序列使得该序列的最大子段和最小化。
称两个序列不同,当且仅当这两个序列有任意一个位置上的数不同。
答案对 998244353 取模。
1≤n≤104,ai∈1,−1。
题解
显然答案只与 1/0 的数量有关。设 1 的数量为 cnt1, −1 的数量为 cnt2,那么分两类:
-
若 cnt1≤cnt2,最小的最大子段和必然是 1。所以方案数就是把原序列进行排列,使得任意两个 1 不相邻的方案数。参考 oi-wiki 上的页面,答案就等于 (n−cnt1+1cnt1) 也就是 (cnt2+1cnt1)。O(n) 预处理阶乘求组合数即可。
-
若 cnt1>cnt2,最小的最大子段和必然是 cnt1−cnt2,也就是把所有数都加起来。考虑设计 dpi,j 为考虑前 i 个数中有 j 个为 1 的方案数。那么转移就是:第 i 位取 1 的方案数等于 dpi−1,j−1,取 −1 的方案数为 dpi−1,j,加起来即可。转移条件就是 0≤sumi≤sumn,其中 sumi 为前缀和。前缀和小于 0 那么方案数显然应该是 0。注意到交上去会 MLE,所以滚动数组优化一下。
参考代码:
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const int MAX_N = 1e4 + 10, MOD = 998244353;
int dp[2][MAX_N], fac[MAX_N];
inline ll read() {...}
inline void write(ll x) {...}
inline void exgcd(ll a, ll b, ll &x, ll &y) {
if(!b) {x = 1, y = 0; return;}
exgcd(b, a % b, y, x);
y -= a / b * x;
}
inline ll inv(ll v) {
ll x, y;
exgcd(v, MOD, x, y);
return (x % MOD + MOD) % MOD;
}
int main() {
int n = read(), sum = 0, cnt1 = 0, cnt2 = 0;
for(int i = 1; i <= n; i++) {
int x = read();
if(x == 1) cnt1++;
else cnt2++;
sum += x;
}
if(cnt1 <= cnt2) {
fac[0] = fac[1] = 1;
for(int i = 2; i <= n + 1; i++) fac[i] = (1ll * fac[i - 1] * i) % MOD;
ll ans = 1ll * fac[cnt2 + 1] * inv(fac[cnt1]) % MOD * inv(fac[cnt2 - cnt1 + 1]) % MOD;
write(ans);
}
else {
dp[0][0] = 1;
bool now = 0;
for(int i = 1; i <= n; i++) {
now ^= 1;
for(int j = 1; j <= min(cnt1, i); j++) {
int s = 2 * j - i;
if(s >= 0 && s <= sum) dp[now][j] = (dp[now ^ 1][j - 1] + dp[now ^ 1][j]) % MOD;
else dp[now][j] = 0;
}
}
write(dp[now][cnt1]);
}
return 0;
}
[NOIP2020] 排水系统
对于一个城市来说,排水系统是极其重要的一个部分。
有一天,小 C 拿到了某座城市排水系统的设计图。排水系统由 n 个排水结点(它们从 1∼n 编号)和若干个单向排水管道构成。每一个排水结点有若干个管道用于汇集其他排水结点的污水(简称为该结点的汇集管道),也有若干个管道向其他的排水结点排出污水(简称为该结点的排出管道)。
排水系统的结点中有 m 个污水接收口,它们的编号分别为 1,2,…,m,污水只能从这些接收口流入排水系统,并且这些结点没有汇集管道。排水系统中还有若干个最终排水口,它们将污水运送到污水处理厂,没有排出管道的结点便可视为一个最终排水口。
现在各个污水接收口分别都接收了 1 吨污水,污水进入每个结点后,会均等地从当前结点的每一个排出管道流向其他排水结点,而最终排水口将把污水排出系统。
现在小 C 想知道,在该城市的排水系统中,每个最终排水口会排出多少污水。该城市的排水系统设计科学,管道不会形成回路,即不会发生污水形成环流的情况。
1≤n≤105,1≤m≤10,0≤di≤5。
题解
大概是拓扑排序的板题。然后用 __int128
就不用写高精了,方便分数用 gcd
化简。
#include <bits/stdc++.h>
using namespace std;
typedef __int128 ll;
const int MAX_N = 1e5 + 10;
vector<int> G[MAX_N], ans;
int in[MAX_N], n, m;
inline ll read() {...}
inline void write(ll x) {...}
inline ll gcd(ll m, ll n) {while(n != 0) {ll t = m % n; m = n, n = t;} return m;}
inline ll lcm(ll m, ll n) {return m * n / gcd(m, n);}
struct frac {
ll a, b;
frac() {a = 0, b = 1;}
void reduce() {
ll d = gcd(a, b);
a /= d, b /= d;
}
void print() {
write(a), putchar(' ');
write(b), putchar('\n');
}
frac operator + (const frac &t) const {
frac ret;
ret.b = lcm(b, t.b);
ret.a = ret.b / b * a + ret.b / t.b * t.a;
ret.reduce();
return ret;
}
} w[MAX_N];
void topo() {
queue<int> q;
for(int i = 1; i <= n; i++) if(in[i] == 0) q.push(i), w[i].a = 1;
while(!q.empty()) {
int u = q.front(); q.pop();
int out = G[u].size();
if(out > 0) {
w[u].b *= out;
for(int i = 0; i < out; i++) {
int v = G[u][i];
w[v] = w[v] + w[u];
if(--in[v] == 0) q.push(v);
}
}
}
}
int main() {
n = read(), m = read();
for(int u = 1; u <= n; u++) {
int d = read();
for(int i = 1; i <= d; i++) {
int v = read();
G[u].push_back(v);
in[v]++;
}
if(d == 0) ans.push_back(u);
}
topo();
for(int i = 0; i < ans.size(); i++) w[ans[i]].print();
return 0;
}
Story
给定质数 p,有 T 次询问,每次给出非负整数 a,d,n,求:
(n−1∏i=0(a+id))modpT,p≤107
题解
原式等价于 dnn−1∏i=0(ad+i),然后考虑这样一个事实:
n−1∏i=0(M+i)=(M+n−1)!(M−1)!于是直接预处理阶乘即可。时间复杂度 O(Tlogp+p)。
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const int MAX_N = 1e7 + 10;
ll fac[MAX_N], T, p;
inline ll read() {...}
inline void write(ll x) {...}
void exgcd(ll a, ll b, ll &x, ll &y) {
if(!b) {x = 1, y = 0; return;}
exgcd(b, a % b, y, x);
y -= a / b * x;
}
ll inv(ll v) {
ll x, y;
exgcd(v, p, x, y);
return (x % p + p) % p;
}
ll qpow(ll a, ll b) {
ll ret = 1;
a = (a % p + p) % p;
while(b) {
if(b & 1) ret = (ret * a) % p;
a = (a * a) % p;
b >>= 1;
}
return ret;
}
int main() {
T = read(), p = read();
fac[0] = 1;
for(int i = 1; i <= p; i++) fac[i] = (fac[i - 1] * i) % p;
while(T--) {
ll a = read(), d = read(), n = read();
if(!d) write(qpow(a, n));
else {
ll M = a * inv(d) % p;
if(M + n - 1 > p) write(0);
else write(fac[M + n - 1] * inv(fac[M - 1]) % p * qpow(d, n) % p);
}
putchar('\n');
}
return 0;
}
- 上一篇 OI笔记 | 数列分块笔记
- 下一篇 OI笔记 | 杂类模板
♥
v1.5.2