OI笔记 | 2022.10 做题记录(一)

其中的 dp模板题 被转移到新的专题中了。

以后每 $10$ 题开一篇文章。

停课期间的安排

[2022-10-12] Ver. 1

针对 10 月的赛前时间安排如下。

文化课

目标

学习清单

考虑学习或练习以下内容:

至少的刷题强度:

  1. 一天 $2\sim 3$ 道橙题(从真题里选,限时切掉,不看题解);

  2. 一天 $1\sim 2$ 道黄题(在上述内容的题单中选,专题练习,少看题解);

  3. 一天 $0\sim 2$ 道绿题(在上述范围内都可尝试做,想不出来直接看题解)。

  4. 蓝题及以上的题不作安排。如果恰好双倍经验或优化搜索可以调调看。

[NOIP2003 普及组] 麦森数

洛谷 P1045

输出 $2^P - 1$ 的位数和后 $500$ 位数字,其中 $1 \times 10^3< P <3.1\times 10^6$。

题解

第一问是数学推导。由于 $2$ 的幂不可能以 $0$ 结尾, 可知 $2^P-1$ 的位数与 $2^P$ 的位数相同。我们知道 $10^n$ 的位数是 $n + 1$,所以考虑转化: $2^P=(10^{\log_{10}2})^P=10^{P\log_{10}2 }$,故位数为 $P\times \log_{10}2+1$。

第二问是一个高精快速幂,见代码即可。

#include <bits/stdc++.h>
using namespace std;
typedef long long ll;

struct BigInt {
	int a[1005];
	BigInt() {memset(a, 0, sizeof(a));}
	void out() {
		int k = 500;
		for(int i = 1; i <= 10; i++) {
			for(int j = 1; j <= 50; j++)	putchar(a[k--] + '0');
			putchar('\n');
		}
	}
	BigInt operator * (const BigInt &t) const {
		BigInt ret;
		for(int i = 1; i <= 500; i++) {
			for(int j = 1; j <= 500; j++) {
				ret.a[i + j - 1] += a[i] * t.a[j];
				ret.a[i + j] += ret.a[i + j - 1] / 10;
				ret.a[i + j - 1] %= 10; 
			}
		}
		return ret;
	}
};
inline ll read() {...}
inline void write(ll x) {...}

BigInt qpow(int p) {
	BigInt ret, t;
	ret.a[1] = 1; t.a[1] = 2;
	while(p) {
		if(p & 1)	ret = ret * t;
		t = t * t;
		p >>= 1;
	}
	return ret;
}

int main() {
	int p = read();
	write(int(log10(2) * p + 1)); putchar('\n');
	BigInt ans = qpow(p); ans.a[1]--;
	ans.out();
	return 0;
}

[NOIP2004 普及组] FBI 树

我们可以把由 0 和 1 组成的字符串分为三类:全 0 串称为 B 串,全 1 串称为 $I$ 串,既含 0 又含 1 的串则称为 $F$ 串。

$FBI$ 树是一种二叉树,它的结点类型也包括 $F$ 结点,$B$ 结点和 $I$ 结点三种。由一个长度为 $2^N$ 的 01 串 $S$ 可以构造出一棵 $FBI$ 树 $T$,递归的构造方法如下:

  1. $T$ 的根结点为 $R$,其类型与串 $S$ 的类型相同;
  2. 若串 $S$ 的长度大于 $1$,将串 $S$ 从中间分开,分为等长的左右子串 $S_1$ 和 $S_2$;由左子串 $S_1$ 构造 $R$ 的左子树 $T_1$,由右子串 $S_2$ 构造 $R$ 的右子树 $T_2$。

现在给定一个长度为 $2^N$ 的 01 串,请用上述构造方法构造出一棵 $FBI$ 树,并输出它的后序遍历序列。

题解

按照题意模拟即可。

#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const int MAX_N = (1 << 11) + 1;
bool a[MAX_N];
char tree[MAX_N];

void build_tree(int p, int l, int r) {
	bool B = 0, I = 0;
	for(int i = l; i <= r; i++) {
		if(a[i] == 0)	B = 1;
		if(a[i] == 1)	I = 1;
	}
	if(B && I)	tree[p] = 'F';
	else if(B && !I)	tree[p] = 'B';
	else if(!B && I)	tree[p] = 'I';
	if(l != r) {
		int m = l + ((r - l) >> 1);
		build_tree((p << 1), l, m);
		build_tree((p << 1) | 1, m + 1, r);
	}
}

void print_tree(int p) {
	if(tree[(p << 1)])	print_tree((p << 1));
	if(tree[(p << 1) | 1])	print_tree((p << 1) | 1);
	putchar(tree[p]);
} 

int main() {
	int n;
  	cin >> n;
  	char c;
	for(int i = 1; i <= (1 << n); i++)	cin >> c, a[i] = (c - '0');
	build_tree(1, 1, (1 << n));
	print_tree(1);		
	return 0;
}

[CSP-S2019 江西] 和积和

给定两个下标从 $1$ 到 $n$ 编号的序列 $a_i,b_i$,定义函数 $S(l,r)(1\le l\le r\le n)$ 为:

\[\sum_{i=l}^r a_i\times \sum_{i=l}^r b_i\]

请你求出下列式子的值:

\[\sum_{l=1}^n \sum_{r=l}^n S(l,r)\]

由于答案可能很大,你只需要给出答案模 $10^9+7$ 后的结果。

数据范围:$3\le n\le 5\times 10^5$ , $1\le a_i,b_i\le 10^9$。

题解

这题主要是式子有点难推,推了一个小时才推出来。另外取模如果取模不当的话分数会比暴力还低。

首先用 $sa$,$sb$ 存前缀和,于是得到以下式子:

\[ans=\sum_{l=1}^n \sum_{r=l}^n [(sa[r]-sa[l-1])(sb[r]-sb[l-1])]=\\ \sum\limits_{l=1}^n \sum\limits_{r=l}^n(sa[r]sb[r]+sa[l-1]sb[l-1]-sa[r]sb[l-1]-sa[l-1]sb[r])\]

利用这个式子可以拿到 $70$ 暴力分。

继续考虑如何只枚举左端点。我们用 $smul$ 存 $sa[i]sb[i]$ 的前缀和,这样式子中 $\sum\limits_{r=l}^n(sa[r]sb[r])$ 的部分可以 $O(1)$ 解决 。再用 $ssa$ 存 $sa$ 的前缀和, $ssb$ 存 $sb$ 的前缀和,这样式子中 $\sum\limits_{r=l}^n (sa[r]sb[l-1]+sa[l-1]sb[r])$ 也可以 $O(1)$ 解决。

参考代码:

#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const int MOD = 1e9 + 7;
const int MAX_N = 5e5 + 5;
ll sa[MAX_N], sb[MAX_N], smul[MAX_N], ssa[MAX_N], ssb[MAX_N];
inline ll read() {...}
inline void write(ll x) {...}

int main() {
	int n = read(), x;
	ll ans = 0;
	for(int i = 1; i <= n; i++) {
		x = read();
		sa[i] = (sa[i - 1] + x) % MOD;
		ssa[i] = (ssa[i - 1] + sa[i]) % MOD;
	}
	for(int i = 1; i <= n; i++) {
		x = read();
		sb[i] = (sb[i - 1] + x) % MOD;
		ssb[i] = (ssb[i - 1] + sb[i]) % MOD;
		smul[i] = (smul[i - 1] + (sa[i] * sb[i]) % MOD) % MOD;
	}
	for(int i = 1; i <= n; i++) {
		ans = (ans + smul[n] - smul[i - 1]) % MOD;
		ans = (ans + (n - i + 1) * (sa[i - 1] * sb[i - 1] % MOD)) % MOD;
		ans = (ans - (sa[i - 1] * (ssb[n] - ssb[i - 1]) % MOD) % MOD + MOD) % MOD;
		ans = (ans - (sb[i - 1] * (ssa[n] - ssa[i - 1]) % MOD) % MOD + MOD) % MOD;
	}	
	write(ans);
	return 0;
}

[NOIP2001 普及组] 求先序排列

给出一棵二叉树的中序与后序排列。求出它的先序排列。(约定树结点用不同的大写字母表示,且二叉树的节点个数 $ \le 8$)。

题解

参考代码:

#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
inline ll read() {...}
inline void write(ll x) {...}
void p(string a, string b) {
	if(a.size() <= 0 || b.size() <= 0)	return;
	char root = b[b.size() - 1];
	int k = a.find(root);
	putchar(root);
	p(a.substr(0, k), b.substr(0, k));
	p(a.substr(k + 1), b.substr(k, a.size() - k - 1));
}

int main() {
	string a, b;
	cin >> a >> b;
	p(a, b);
	return 0;
}

[NOIP2001 提高组] 数的划分

将整数 $n$ 分成 $k$ 份,且每份不能为空,任意两个方案不相同(不考虑顺序)。

例如:$n=7$,$k=3$,下面三种分法被认为是相同的。

$1,1,5$;
$1,5,1$;
$5,1,1$.

问有多少种不同的分法。

题解

考虑数据范围较小,可以直接暴力搜索。

#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
int n, k, ans;

inline ll read() {...}
inline void write(ll x) {...}

void dfs(int p, int sum, int l) {
	if(p == k) {
		if(sum == n)	ans++;
		return;
	}
	for(int i = l; i + sum <= n; i++)   dfs(p + 1, sum + i, i);
}

int main() {
	n = read(), k = read();
	dfs(0, 0, 1);
	cout << ans;
	return 0;
}

另外本题还有递推的思路。递推式:$dp_{i,j}=dp_{i-1,j-1}+dp_{i-j,j}$。

[NOIP2021] 报数

报数游戏是一个广为流传的休闲小游戏。参加游戏的每个人要按一定顺序轮流报数,但如果下一个报的数是 $7$ 的倍数,或十进制表示中含有数字 $7$,就必须跳过这个数,否则就输掉了游戏。

在一个风和日丽的下午,刚刚结束 SPC20nn 比赛的小 r 和小 z 闲得无聊玩起了这个报数游戏。但在只有两个人玩的情况下计算起来还是比较容易的,因此他们玩了很久也没分出胜负。此时小 z 灵光一闪,决定把这个游戏加强:任何一个十进制中含有数字 $7$ 的数,它的所有倍数都不能报出来!

形式化地,设 $p(x)$ 表示 $x$ 的十进制表示中是否含有数字 $7$,若含有则 $p(x) = 1$,否则 $p(x) = 0$。则一个正整数 $x$ 不能被报出,当且仅当存在正整数 $y$ 和 $z$ ,使得 $x = yz$ 且 $p(y) = 1$。

例如,如果小 r 报出了 $6$ ,由于 $7$ 不能报,所以小 z 下一个需要报 $8$;如果小 r 报出了 $33$,则由于 $34 = 17 \times 2$,$35 = 7 \times 5$ 都不能报,小 z 下一个需要报出 $36$ ;如果小 r 报出了 $69$,由于 $70 \sim 79$ 的数都含有 $7$,小 z 下一个需要报出 $80$ 才行。

现在小 r 的上一个数报出了 $x$,小 z 想快速算出他下一个数要报多少,不过他很快就发现这个游戏可比原版的游戏难算多了,于是他需要你的帮助。当然,如果小 r 报出的 x 本身是不能报出的,你也要快速反应过来小 r 输了才行。

由于小 r 和小 z 玩了很长时间游戏,你也需要回答小 z 的很多个问题。

题解

首先筛出来不合法的数。我这里采用的是枚举 $7$ 在数字中出现的位置来构造不合法的数,然后把它的倍数筛掉。类似于埃氏筛法,我们知道如果枚举到某个数时它已经被筛掉了,可以直接跳过它。

然后注意预处理一下每个数的后继让其为 $O(1)$,而不能每次询问都向后查找出后继,后者会 TLE。

#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const int MAX_N = 1e7 + 3;
bool f[MAX_N];
int nxt[MAX_N];
inline ll read() {...}
inline void write(ll x) {...}

void init() {
	int p = 1, q = MAX_N / 10;
	for(int i = 1; i <= 7; i++) {
		for(int m = 0; m < p; m++) {
			for(int n = 0; n < q; n++) {
				int t = n * (p * 10) + 7 * p + m;
				if(f[t])	continue;
				for(int i = 1; i * t <= MAX_N; i++)	f[i * t] = 1;
			}
		}
		p *= 10;
		q /= 10;
	}
	int t = MAX_N - 2;
	for(int i = MAX_N - 3; i >= 1; i--) {
		if(f[i])	nxt[i] = -1;
		else	nxt[i] = t, t = i;	
	}
}

int main() {
	init();
	int T = read();
	while(T--) {
		int x = read();
		write(nxt[x]);
		putchar('\n');
	}
	return 0;
}

[NOIP2013 提高组] 积木大赛

洛谷 P1969

春春幼儿园举办了一年一度的“积木大赛”。今年比赛的内容是搭建一座宽度为 $n$ 的大厦,大厦可以看成由 $n$ 块宽度为 $1$ 的积木组成,第 $i$ 块积木的最终高度需要是 $h_i$。

在搭建开始之前,没有任何积木(可以看成 $n$ 块高度为 $0$ 的积木)。接下来每次操作,小朋友们可以选择一段连续区间 $[l, r]$,然后将第 $L$ 块到第 $R$ 块之间(含第 $L$ 块和第 $R$ 块)所有积木的高度分别增加 $1$。

小 M 是个聪明的小朋友,她很快想出了建造大厦的最佳策略,使得建造所需的操作次数最少。但她不是一个勤于动手的孩子,所以想请你帮忙实现这个策略,并求出最少的操作次数。

题解

对于本题我们可以纯粹模拟贪心水过,但是复杂度过高,不太优秀。

考虑使用递推: 考虑用 $f_i$ 表示考虑到第 $i$ 块积木时的操作次数,那么如果 $h_i \le h_{i - 1}$, 那么在搭好第 $i-1$ 块时顺带就能搭好第 $i$ 块积木,所以 $f_i=f_{i-1}$。否则,只需在搭好第 $i-1$ 块所用次数的基础上加上多余高度即可,所以 $f_i = f_{i - 1} + (h_i - h_{i - 1})$。

#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const int MAX_N = 100005;
int a[MAX_N], f[MAX_N];
inline ll read() {...}
inline void write(ll x) {...}

int main() {
	int n = read(), ans = 0;
	for(int i = 1; i <= n; i++)	a[i] = read();
	f[1] = a[1];
	for(int i = 2; i <= n; i++) {
		if(a[i] <= a[i - 1])	f[i] = f[i - 1];
		else	f[i] = f[i - 1] + a[i] - a[i - 1];
	}
	write(f[n]);
	return 0;
}

白头鹰

白头鹰作为美丽国的国鸟,每天都会进行数学演算的工作。

今天,他得到了3个整数$a, b, p$,其中$p$是一个质数,以及一个函数$f(x, y)$。

函数$f(x,y)$如下定义:

白头鹰获得了一个整数$k$,且需要计算一下式子:

\[f(f(...f(f(f(k, k-1), k-2), ..., 2), 1)\]

对于$100\%$的数据:$2 \leq p \leq 10^{9}+7, 1 \leq a, b \leq min(p-1, 10^{5}), 2 \leq k \leq 10^{18}$。

题解

首先我们要弄清楚逆元怎么求。

乘法逆元可定义为 $ax\equiv 1(\bmod p)$ 的解。这个方程等价为 $ax+py=1$,如果 $p$ 为质数,它等价为 $ax+py=\gcd(a,p)$,形成了 exgcd 的标准形式。所以容易写出:

void exgcd(ll a, ll b, ll &x, ll &y) {
	if(!b) {x = 1, y = 0; return;}
	exgcd(b, a % b, x, y);
	ll t = x; x = y; y = t - (a / b) * y;
} 
inline ll inv(ll v, ll p) {
	ll x, y;
	exgcd(v, p, x, y);
	return (x % p + p) % p;
}

本题的时间复杂度主要来自于 $k$ 的值。对于比较小的 $k$,可以直接计算答案,复杂度为 $O(k\log v)$。此外,打表观察发现当 $k \ge a+ 1$ 时,答案总是不变的,所以我们加一句 if(k > a + 1) k = a + 1; 再注意一下每一步取模(防止爆 long long)即可过此题。这个规律怎么证明暂时不懂。

参考代码:

#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
ll a, b, p, k;
inline ll read() {...}
inline void write(ll x) {...}
void exgcd(ll a, ll b, ll &x, ll &y) {...} 
inline ll inv(ll v, ll p) {...}
inline ll f(ll x, ll y) {
	ll v = (a * b % p + x * y % p) % p;
	if(v % p == 0)	return 1;
	ll u = (a * x % p + b * y % p) % p;
	return u * inv(v, p) % p;
}
int main() {
	a = read(), b = read(), p = read(), k = read();
	if(k > a + 1)	k = a + 1;
	ll t = f(k, k - 1);
	for(int i = k - 2; i >= 1; i--)	t = f(t, i);
	write(t);
	return 0;
}

[CSP-J2019 江西] 次大值

洛谷 P5682

给出 $n$ 个正整数。求出

\[a_i \bmod a_j (1 \le i,j \le n \wedge i \neq j)\]

的所有结果中的严格次大值是多少。

题解

考虑排序后使用函数 unique 去重,可知答案为 $\max(a_{n-2}, a_{n} \bmod a_{n-1}) $。

这是因为: 最大值一定为 $a_{n-1}$ ($a_{n-1} \bmod a_{n}=a_{n-1}$)。

并且有

  1. $a_{n-2}=a_{n-2}\bmod a_{n-1}<a_{n-1}$;

  2. $a_{n}\bmod a_{n-1} < a_{n-1}$。

可以证明,其他结果都比这两种结果更小。

参考代码:

#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const int MAX_N = 2e5 + 10;
int a[MAX_N];
inline ll read() {...}
inline void write(ll x) {...}
int main() {
	int n = read();
	for(int i = 1; i <= n; i++)	a[i] = read();
	sort(a + 1, a + n + 1);
	n = unique(a + 1, a + n + 1) - a - 1;
	write(max(a[n - 2], a[n] % a[n - 1]));
	return 0;
}

逆序对

洛谷 P1908

给定一段正整数序列,求出其中逆序对的个数。逆序对定义为序列中 $a_i>a_j$ 且 $i<j$ 的有序对。

题解

我们在做归并排序的时候顺带把答案统计一下。也就是若合并时出现左半部分当前指向的数 $a_i$ $>$ 右半部分当前指向的数 $a_j$ 的情况,那么 $mid\to r$ 的每一个数都可以与 $a_j$ 构成逆序对,所以对答案有 $mid - i + 1$ 的贡献。

时间复杂度: $O(n\log n)$。

参考代码:

#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const int MAX_N = 5e5 + 10;
int a[MAX_N], tmp[MAX_N];
ll ans;
inline ll read() {...}
inline void write(ll x) {...}
void merge(int l, int r) {
	if(l == r)	return;
	int mid = (l + r) >> 1;
	merge(l, mid);
	merge(mid + 1, r);
	int i = l, j = mid + 1;
	for(int k = l; k <= r; k++) {
		if(a[i] <= a[j] && i <= mid || j > r)	tmp[k] = a[i++];
		else ans += mid - i + 1, tmp[k] = a[j++];
	}
	for(int k = l; k <= r; k++)	a[k] = tmp[k];
}

int main() {
	int n = read();
	for(int i = 1; i <= n; i++)	a[i] = read();
	merge(1, n);
	write(ans);
	return 0;
}

Joy © 2023 Powered by Jekyll and Theme by solid

今日诗词API Valine 评论管理 Github