NOIP 膜你赛 做题记录
\(\texttt{Day 1 T1}\)
题目大意:
定义 \(f(x)\) 表示正整数 \(x\) 在十进制下的数位和,如 \(f(114514)=1+1+4+5+1+4=16\)。
现在小 \(C\) 有个好数集合 \(S\),他给出三个正整数 \(n,x,k\),并告诉小 \(D\) 这个集合的性质:
-
\(x\in S\)。
-
如果正整数 \(y\) 满足 \(y\le n,y−f(y)\times k\in S\),则 \(y\in S\)。
-
如果正整数 \(y\) 满足 \(y\le n\),且存在正整数 \(c>1\)
满足 \(y^c\in S\),则 \(y\in S\)。
现在小 \(D\) 要猜这个集合中的数,每次猜的数不能和之前相同,猜错就结束。你需要告诉他如果采用最优策略,至少能猜对多少次。
输入格式
一行三个正整数 \(n,x,k\)。
输出格式
一行一个整数表示答案。
样例输入
20 8 2
样例输出
7
样例解释
\(\{2,4,8,10,14,16,20\}\subseteq S\)
数据范围
对于所有数据,\(1\le n\le 10^7\),\(1\le x,k\le n\)。
对于前 \(20\%\) 的数据,\(n\le 20\)。
对于前 \(40\%\) 的数据,\(n\le 1000\)。
对于前 \(70\%\) 的数据,\(n\le 10^6\)。
思路
这题正解应该是考虑图论意义,将 \(x\) 当成起点,对于条件 \(2\),先递推求 \(1\sim n\) 所有数的数位和,然后依次扫描 \(1\sim n\) 建边,最多就 \(O(n)\) 条,再根据条件 \(3\) 建边,边数为 \(\log_2n + \log_3n + \cdots + \log_{\lfloor\sqrt n\rfloor}n\)。
写个程序跑出来大概是 \(2.8\sqrt n\) 的样子。
但我只会证明它严格小于 \(n\),也不知道题解里写的 \(O(\sqrt n)\) 是怎么算出来的。
所以边数大概是 \(O(n)\) 的,再用 bfs 统计从 \(x\) 出发能到达的点的数量,时间复杂度 \(O(n)\)。
\(\texttt{Code:}\)
#include <bits/stdc++.h>
using namespace std;
const int N = 1e7 + 10, M = 2e8 + 10;
typedef long long ll;
int n, x, k, sq, t, ans;
int h[N], ne[M], e[M], idx;
int num[N];
queue<int> q;
bool vis[N];
inline void add(int a, int b) {
e[idx] = b, ne[idx] = h[a], h[a] = idx++;
}
void bfs() {
q.push(x);
vis[x] = 1;
while(q.size()) {
t = q.front();
q.pop();
for (int i = h[t]; ~i; i = ne[i]) {
int j = e[i];
if (!vis[j]) {
vis[j] = true;
q.push(j);
}
}
}
}
int main() {
memset(h, -1, sizeof h);
scanf("%d%d%d", &n, &x, &k);
for (int i = 1; i <= n; i++)
num[i] = i % 10 + num[i / 10];
sq = sqrt(n);
for(int i = 2; i <= sq; i++)
for (ll j = 1ll * i * i; j <= n; j *= i)
add(j, i);
int cnt = 0;
for (int i = 1; i <= n; i++) {
t = i - num[i] * k;
if(t > 0) add(t, i);
}
bfs();
for (int i = 1; i <= n; i++)
vis[i] ? ans++ : 1;
printf("%d", ans);
return 0;
}
但考场上我写了一种玄学做法:开个 bool 数组,一直从前往后扫并根据规则更新那些数在集合中,若扫完后答案和扫之前相同,说明更新完毕,退出。
时间复杂度 \(O(kn)\),\(k\) 是一个较小的常数(和 spfa 学的)。
\(\texttt{Code:}\)
#include <ctime>
#include <cmath>
#include <iostream>
using namespace std;
const int N = 10000010;
typedef long long ll;
int n, x, k;
int sum[N];
bool st[N];
int ans;
void check(int x) {
ll pow = 1ll * x * x;
while(pow <= n) {
if(st[pow]) {
st[x] = true;
ans++;
return ;
}
pow *= x;
}
}
void check2(int x) {
if(x - k * sum[x] >= 2 && st[x - k * sum[x]]) {
ans++;
st[x] = true;
}
}
int main() {
scanf("%d%d%d", &n, &x, &k);
if(x == 1) {
puts("1");
return 0;
}
for(int i = 1; i <= n; i++)
sum[i] = i % 10 + sum[i / 10];
st[x] = true;
ans++;
int limit = (int)sqrt(n);
//玄学
while(1) {
int las = ans;
for(int i = 2; i <= limit; i++)
if(!st[i])
check(i);
for(int i = 1; i <= n; i++)
if(!st[i])
check2(i);
if(ans == las) break;
}
printf("%d\n", ans);
return 0;
}
\(\texttt{Day 2 T1}\)
题目描述
棱镜宫殿的大门前有一个奇妙的装置。这个装置上有若干个转轮,每个转轮上有一个 \(0\) 到 \(9\) 之间的数字。这些转轮从左到右排开,形成了一个正整数 \(N\)。
小凯为了进入棱镜宫殿,他可以进行如下操作一次:
选择其中一些转轮转动,对于每个选择的转轮,操作之后会独立的等概率的变化成 \(0\) 到 \(9\) 之间的任意一个数字(操作后可能会出现前导零)。
如果操作后新的 \(N\) 更大了,宫殿的大门会打开,你需要帮助小凯选择一些转轮,最大化这个概率,对 \(998244353\) 取模后输出。
输入格式
一行一个正整数 \(N\)。
输出格式
输出一行一个整数,表示最大的概率。
样例输入
115
样例输出
970293512
样例解释
最优的操作是选择所有转轮,概率为 \(8/10+1/10×8/10+1/100×4/10=884/1000\)。
数据范围与约定
- 对于所有数据,有:\(1\le N<10^{2×10^5}\)
- 输入没有前导 \(0\)
思路
做一个简单的贪心:选择数值尽量小的改变,概率会尽量大。
令 \(l = 1, r = k\),\(k\) 为该数有几位。
采取这样一种策略:
- 选取 \([l, r]\) 中数位最小且最靠前的那位,记为 \(x\),操作它;
- 令 \(l = x + 1\);
- 反复执行 \(1,2\) 直到 \(l = r + 1\)。
为什么这样是对的呢?
首先找最小值肯定是对的,但需要考虑怎么找。
先钦定好寻找顺序。
假设我们现在找到最靠前的最小值为一个数位上的数 \(now\),往前找到最小值 \(pre\),往后找到最小值 \(nex\)。
设先操作 \(now\) 再操作 \(pre\) 成功的概率为 \(P_1\),先操作 \(now\) 再操作 \(nex\) 成功的概率为 \(P_2\)。
根据条件概率可得:
\(P_1 = \frac{9 - now}{10}\times\frac{1}{10} + \frac{1}{10}\times\frac{9 - pre}{10} + \frac{9 - now}{10}\times\frac{9 - pre}{10}\)
\(P_2 = \frac{9 - now}{10} + \frac{1}{10}\times\frac{9 - nex}{10}\)
两者作差,得:
\(P_2 - P_1 = \frac{pre\cdot (10 - now) - nex}{100}\)
根据 \(now < pre,now\le nex,now,pre,nex\in \{0, 1, \cdots,9\}\),可得:\(P_2 > P_1\) 恒成立。
所以从前往后选择是更优的。
令 \(x_1, x_2,\cdots,x_k\) 表示选择的数位,\(x_1\) 是最高位,\(x_k\) 是最低位,则概率是 \(\sum\limits_{i = 1}^k (9 - x_i)10^{-i}\)。
这是一个字典序的形式,不断贪心的取最靠前的后缀最小值就行。
时间复杂度为 \(O(\log n)\)。
\(\texttt{Code:}\)
#include<bits/stdc++.h>
using namespace std;
const int N = 200010, mod = 998244353;
typedef long long ll;
int n;
char s[N];
ll invs[N];
int minn[N];
int pos[N];
ll inv(ll a, ll b = mod - 2) {
ll ans = 1, base = a % mod;
while(b) {
if(b & 1) ans = ans * base % mod;
base = base * base % mod;
b >>= 1;
}
return ans;
}
int main() {
// freopen("ex_opt3.in", "r", stdin);
// freopen("ans.out", "w", stdout);
scanf("%s", s + 1);
n = strlen(s + 1);
invs[1] = inv(10);
for(int i = 2; i <= n; i++)
invs[i] = invs[i - 1] * invs[1] % mod;
minn[n] = s[n] - '0';
pos[n] = n;
for(int i = n - 1; i; i--) {
minn[i] = min(minn[i + 1], s[i] - '0'); //求后缀最小值
if(minn[i + 1] >= s[i] - '0')
minn[i] = s[i] - '0', pos[i] = i;
else minn[i] = minn[i + 1], pos[i] = pos[i + 1];
}
int l = 1, cnt = 1;
ll res = 0;
while(l <= n) {
res = (res + 1ll * (9 - minn[l]) * invs[cnt]) % mod;
cnt++, l = pos[l] + 1;
}
printf("%lld\n", res);
return 0;
}
\(\texttt{Day 3 T1}\)
题目描述
有两个长度为 \(n\) 的序列 \(a_1\sim a_n,b_1\sim b_n\)
,以及一个数 \(type\in{0,1}\)(具体意义见输入格式)。
茵蒂克丝想要求出区间 \([l,r]\) 的一个子区间 \([l',r']\)(即 \(l\le l'\le r'\le r\)),使得 \(\frac{\sum^{r'}\limits_{i=l'}a_i}{\sum^{r'}\limits_{i=l'}b_i}\) 的值最大。茵蒂克丝不希望取出的子区间过小,于是她要求出在满足 \(r'−l'+1\ge k\) 的条件下的最大值。
茵蒂克丝想要求出 \(q\) 个给定区间的最优区间。你需要帮助她求出每组询问的答案。
输入格式
第一行四个数 \(n,k,q,type\)。
第二行 \(n\) 个正整数 \(a_1\sim a_n\)。
第三行 \(n\) 个正整数 \(b_1\sim b_n\)。
接下来 \(q\) 行,每行两个加密而成的数 \(l_0,r_0\)。
设上一次询问的答案的分子为 \(lastans\)(如果之前没有询问则是 \(0\)),则 \(l = (l_0\oplus(lastans\times type))\bmod n + 1, r = (r_0\oplus(lastans\times type))\bmod n + 1\)。如果 \(l>r\),则交换 \(l,r\)(\(\oplus\) 表示二进制下的异或运算)。
输出格式
输出 \(q\) 行,每行形如 \(a/b\) 的形式的最简分数,即 \(\gcd(a,b)=1\)。
如果不存在答案则输出 \(0/1\)。
样例 1 输入
5 2 6 0
2 4 1 3 7
6 1 5 2 4
1 2
1 4
3 3
2 4
2 3
1 3
样例 1 输出
5/6
5/3
0/1
5/3
4/7
1/1
样例 2 输入
5 2 6 1
2 4 1 3 7
6 1 5 2 4
1 2
1 4
3 3
2 4
2 3
1 3
样例 2 输出
5/6
5/3
0/1
5/3
5/6
5/3
数据范围与约定
对于 \(100\%\) 的测试数据,\(1\le n, q\le 10^6, 1\le k\le 20, 1\le a_i,b_i\le 10^7,1\le l_0,r_0\le n\)。
\(Subtask1(20\%):n,q≤300\)。
\(Subtask2(30\%):n,q≤3000\)。
\(Subtask3(20\%):n,q≤105,type=0\)。
\(Subtask4(15\%):k=1\)。
\(Subtask5(15\%)\): 无特殊限制。
需要注意,即使 \(type=0\),最终询问的 \([l,r]\) 也会与输入给出的不同。
思路:
首先考虑部分分,\(Subtask1\) 为 \(O(n^3)\) 暴力,直接打。
\(Subtask2\) 需要用 \(O(n^2)\sim O(1)\) 区间 dp。
设(pair类型的) \(dp_{i, j}\) 表示区间左端点为 \(j\),区间长度为 \(i\) 的最佳区间的分子和分母。
状态转移方程为:
状态数量为 \(n^2\),转移 \(O(1)\),先预处理 \(O(n^2)\),然后 \(O(1)\) 询问。
然后注意到 \(k\) 特别小,不妨从这上面下手。
引理:若 \(\frac{a}{b}\le\frac{c}{d}\),则 \(\frac{a}{b}\le \frac{a + c}{b + d}\le \frac{c}{d}\)。
证明很简单,从溶液浓度方面来理解:一杯甜度为 \(\frac{a}{b}\) 的糖水兑上一杯甜度为 \(\frac{c}{d}\) 的糖水,得到的糖水的甜度一定介于两者之间。
所以最优区间的长度一定小于 \(2k\),否则可以分裂成两个合法区间,其中必然有一个不劣。
这时有用的区间就只剩下 \(O(nk)\) 个了。
可以建立 \(k\) 个 ST 表分别处理长度为 \(k\sim 2k - 1\) 的最优区间,但这样无法过掉 \(n\le 10^6\) 的数据,会 MLE。
回想区间 dp 的做法,发现两者可以做一个平衡,将区间长度小于等于 \(2k\) 的区间用区间 dp 处理,这样状态数优化为 \(O(nk)\);区间长度大于 \(2k\) 的用 ST 表区间查询即可。
具体地,设 \(t = 2k - 1\),对于一组询问 \([l, r]\),考虑所有的 \(l'\le r - t + 1\),以这些位置为左端点的有用区间一定被询问区间包含。可以对每个左端点预处理其的最优区间,然后使用 ST 表询问。这一部分复杂度为 \(O(nk + n\log n)\)。
对于 \(l' > r - t + 1\),我们可以优化暴力的区间 dp。注意到所有在此时访问到的区间的长度均 \(< t\),我们可以 \(O(nk)\) 地 dp。
时间复杂度为 \((nk + n\log n + q)\)。
\(\texttt{Code:}\)
#include <bits/stdc++.h>
#define x first
#define y second
using namespace std;
const int N = 1000010;
typedef long long ll;
typedef pair<int, int> PII;
int n, k, q, type;
ll suma[N], sumb[N];
PII dp[21][N];
int log_2[N];
void init_log_2() {
log_2[1] = 0;
for(int i = 2; i <= n; i++)
log_2[i] = log_2[i / 2] + 1;
}
inline PII cmp(PII a, PII b) {
if(!a.x && !a.y) return b;
if((ll)a.x * b.y < (ll)b.x * a.y) return b;
return a;
}
ll gcd(ll a, ll b) {
if(!b) return a;
return gcd(b, a % b);
}
PII f[21][N];
void init() {
for(int j = 1; j <= log_2[n]; j++)
for(int i = 1; i + (1 << j) - 1 <= n; i++)
f[j][i] = cmp(f[j - 1][i], f[j - 1][i + (1 << j - 1)]);
}
inline PII query(int l, int r) {
int t = log_2[r - l + 1];
return cmp(f[t][l], f[t][r - (1 << t) + 1]);
}
int main() {
scanf("%d%d%d%d", &n, &k, &q, &type);
init_log_2();
for(int i = 1; i <= n; i++) {
scanf("%d", &suma[i]);
suma[i] += suma[i - 1];
}
for(int i = 1; i <= n; i++) {
scanf("%d", &sumb[i]);
sumb[i] += sumb[i - 1];
}
for(int len = k; len <= k * 2; len++)
for(int l = 1; l + len - 1 <= n; l++) {
int r = l + len - 1;
int sa = suma[r] - suma[l - 1];
int sb = sumb[r] - sumb[l - 1];
PII tmp = {sa, sb};
if(len > k) dp[len - k][l] = cmp(cmp(dp[len - k - 1][l], dp[len - k - 1][l + 1]), tmp);
else dp[len - k][l] = tmp;
}
for(int i = 1; i <= n - k + 1; i++)
for(int j = i + k - 1; j <= min(i + k * 2 - 1, n); j++)
f[0][i] = cmp(f[0][i], {suma[j] - suma[i - 1], sumb[j] - sumb[i - 1]});
init();
int l, r, lasans = 0;
while(q--) {
scanf("%d%d", &l, &r);
l = (l ^ (lasans * type)) % n + 1, r = (r ^ (lasans * type)) % n + 1;
if(l > r) swap(l, r);
if(r - l + 1 < k) {
printf("0/1\n");
lasans = 0;
continue;
}
PII ans = {0, 0};
int limit = r - 2 * k + 1;
if(limit >= l) ans = query(l, limit);
else limit = l - 1;
ans = cmp(ans, dp[r - limit - k][limit + 1]);
int d = gcd(ans.x, ans.y);
ans.x /= d, ans.y /= d;
printf("%d/%d\n", ans.x, ans.y);
lasans = ans.x;
}
return 0;
}
\(\texttt{Day 4 T1}\)
题目描述
菜汪酱是天文学家,喜欢观察行星的运动。
菜汪酱发现所在星系的行星都会绕着恒星转动。一共有 \(n\) 颗行星,分布在不同的轨道上。为了方便记录,菜汪酱把这些行星按照到恒星的距离依次标记为 \(1,2,\cdots,n\)。
通过对行星运动规律的长时间总结和归纳,菜汪酱发现行星会在一个以恒星为圆点的正圆形轨道上顺时针转动。并且通过一系列的观察,菜汪酱得到了所有行星的周期。更具体地,菜汪酱发现第 \(i\) 颗行星的周期为 \(a_i\) 个时间单位。由于某种神秘原因,菜汪酱发现所有 \(a_i\) 互不相同。
你可能听说过 \(n+1\) 星连珠的说法。更具体地,如果存在一条直线使得恒星和 \(n\) 颗行星都在这一条直线上,那么就是认为这一时刻出现了 \(n+1\) 星连珠。也就是说,就算不是所有行星都在恒星的同一侧,只要在一条直线上,也认为出现了 \(n+1\) 星连珠。
菜汪酱认为出现这种情况是美丽的,因此想要知道发生 \(n+1\) 星连珠的时间。幸运的是,现在就已经出现了 \(n+1\) 星连珠,因此菜汪酱想要知道下一次在这一条直线上出现 \(n+1\) 星连珠是多少时间单位之后。
你只需要验算就可以了,所以只需要你输出对 \(998244353\) 取模的结果。
当然结果可能并非整数,但是可以证明结果一定是有理数,因此如果最后的答案是 \(p/q\),\(\gcd(p,q)=1\),那么你的输出 \(t\) 需要满足 \(0\le t < 998244353\),\(t\times q\equiv p\pmod{998244353}\)。题目保证答案存在且唯一。
输入格式
第一行一个整数 \(n\)。
接下来一行 \(n\) 个整数 \(a_1,\cdots,a_n\)。
输出格式
一行一个整数表示答案。
样例输入
2
114 514
样例输出
14649
数据范围
对于 \(20\%\) 的数据,\(n=2\)。
对于 \(40\%\) 的数据,\(n\le 100,a_i\le 10^9\)。
对于 \(100\%\) 的数据,\(1\le n\le 5000,1\le a_i\le 10^{18}\)。
思路:
很容易将原题面转化为:
给定 \(n\) 个正整数 \(\{a_1, a_2,\cdots,a_n\}\),求它们的最小公倍数的 \(\frac{1}{2}\),结果对 \(998244353\) 取模。
首先要声明一点,不能在做 \(\operatorname{lcm}\) 时顺便取模!
知道了这一点后,要么使用高精度算 \(\gcd\),要么就用接下来的优美方法。
鉴于高精度除法和取模太难写,在考场上很难写对,所以学习优美的另一种算法。
维护 \(\operatorname{lcm}(a_1, a_2,\cdots,a_i) = b_1\times b_2\times\cdots \times b_i\),当新加进来一个数 \(a_{i + 1}\) 时,推式子:
这里的 \(b_1\times b_2\times\cdots\times b_i\bmod a_{i + 1}\) 每次都要计算,所以时间复杂度为 \(O(n\log V + n^2)\),其中 \(V\) 表示值域。
\(\texttt{Code:}\)
#include <iostream>
using namespace std;
typedef unsigned long long ull;
const int N = 5010, mod = 998244353, inv2 = (mod + 1) / 2;
int n;
ull a[N], b[N];
inline ull gcd(ull a, ull b) {
if(!b) return a;
return gcd(b, a % b);
}
int main() {
scanf("%d", &n);
ull x;
for(int i = 1; i <= n; i++) {
scanf("%llu", &x);
ull mul = 1;
for(int j = 1; j < i; j++)
mul = (__int128)mul * b[j] % x;
b[i] = x / gcd(mul, x);
}
ull res = 1;
for(int i = 1; i <= n; i++)
res = (__int128)res * b[i] % mod;
printf("%llu", res * inv2 % mod);
return 0;
}