NOIP 膜你赛 做题记录

Day 1 T1

题目大意:

定义 f(x) 表示正整数 x 在十进制下的数位和,如 f(114514)=1+1+4+5+1+4=16

现在小 C 有个好数集合 S,他给出三个正整数 n,x,k,并告诉小 D 这个集合的性质:

  1. xS

  2. 如果正整数 y 满足 yn,yf(y)×kS,则 yS

  3. 如果正整数 y 满足 yn,且存在正整数 c>1
    满足 ycS,则 yS

现在小 D 要猜这个集合中的数,每次猜的数不能和之前相同,猜错就结束。你需要告诉他如果采用最优策略,至少能猜对多少次。

输入格式

一行三个正整数 n,x,k

输出格式

一行一个整数表示答案。

样例输入

20 8 2

样例输出

7

样例解释

{2,4,8,10,14,16,20}S

数据范围

对于所有数据,1n1071x,kn

对于前 20% 的数据,n20

对于前 40% 的数据,n1000

对于前 70% 的数据,n106

思路

这题正解应该是考虑图论意义,将 x 当成起点,对于条件 2,先递推求 1n 所有数的数位和,然后依次扫描 1n 建边,最多就 O(n) 条,再根据条件 3 建边,边数为 log2n+log3n++lognn

写个程序跑出来大概是 2.8n 的样子。

但我只会证明它严格小于 n,也不知道题解里写的 O(n) 是怎么算出来的。

所以边数大概是 O(n) 的,再用 bfs 统计从 x 出发能到达的点的数量,时间复杂度 O(n)

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 学的)。

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;
}

Day 2 T1

题目描述

棱镜宫殿的大门前有一个奇妙的装置。这个装置上有若干个转轮,每个转轮上有一个 09 之间的数字。这些转轮从左到右排开,形成了一个正整数 N

小凯为了进入棱镜宫殿,他可以进行如下操作一次:

选择其中一些转轮转动,对于每个选择的转轮,操作之后会独立的等概率的变化成 09 之间的任意一个数字(操作后可能会出现前导零)。
如果操作后新的 N 更大了,宫殿的大门会打开,你需要帮助小凯选择一些转轮,最大化这个概率,对 998244353 取模后输出。

输入格式

一行一个正整数 N

输出格式

输出一行一个整数,表示最大的概率。

样例输入

115

样例输出

970293512

样例解释

最优的操作是选择所有转轮,概率为 8/10+1/10×8/10+1/100×4/10=884/1000

数据范围与约定

  • 对于所有数据,有:1N<102×105
  • 输入没有前导 0

思路

做一个简单的贪心:选择数值尽量小的改变,概率会尽量大。

l=1,r=kk 为该数有几位。

采取这样一种策略:

  1. 选取 [l,r] 中数位最小且最靠前的那位,记为 x,操作它;
  2. l=x+1
  3. 反复执行 1,2 直到 l=r+1

为什么这样是对的呢?

首先找最小值肯定是对的,但需要考虑怎么找。

先钦定好寻找顺序。

假设我们现在找到最靠前的最小值为一个数位上的数 now,往前找到最小值 pre,往后找到最小值 nex

设先操作 now 再操作 pre 成功的概率为 P1,先操作 now 再操作 nex 成功的概率为 P2

根据条件概率可得:

P1=9now10×110+110×9pre10+9now10×9pre10

P2=9now10+110×9nex10

两者作差,得:

P2P1=pre(10now)nex100

根据 now<pre,nownex,now,pre,nex{0,1,,9},可得:P2>P1 恒成立。

所以从前往后选择是更优的。

x1,x2,,xk 表示选择的数位,x1 是最高位,xk 是最低位,则概率是 i=1k(9xi)10i

这是一个字典序的形式,不断贪心的取最靠前的后缀最小值就行。

时间复杂度为 O(logn)

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;
}

Day 3 T1

题目描述

有两个长度为 n 的序列 a1an,b1bn
,以及一个数 type0,1(具体意义见输入格式)。

茵蒂克丝想要求出区间 [l,r] 的一个子区间 [l,r](即 llrr),使得 i=lraii=lrbi 的值最大。茵蒂克丝不希望取出的子区间过小,于是她要求出在满足 rl+1k 的条件下的最大值。

茵蒂克丝想要求出 q 个给定区间的最优区间。你需要帮助她求出每组询问的答案。

输入格式

第一行四个数 n,k,q,type

第二行 n 个正整数 a1an

第三行 n 个正整数 b1bn

接下来 q 行,每行两个加密而成的数 l0,r0

设上一次询问的答案的分子为 lastans(如果之前没有询问则是 0),则 l=(l0(lastans×type))modn+1,r=(r0(lastans×type))modn+1。如果 l>r,则交换 l,r 表示二进制下的异或运算)。

输出格式

输出 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% 的测试数据,1n,q106,1k20,1ai,bi107,1l0,r0n

Subtask1(20%):n,q300

Subtask2(30%):n,q3000

Subtask3(20%):n,q105,type=0

Subtask4(15%):k=1

Subtask5(15%): 无特殊限制。

需要注意,即使 type=0,最终询问的 [l,r] 也会与输入给出的不同。

思路:

首先考虑部分分,Subtask1O(n3) 暴力,直接打。

Subtask2 需要用 O(n2)O(1) 区间 dp。

设(pair类型的) dpi,j 表示区间左端点为 j,区间长度为 i 的最佳区间的分子和分母。

状态转移方程为:

dpi,j=cmp(dpi1,j,dpi1,j+1)

状态数量为 n2,转移 O(1),先预处理 O(n2),然后 O(1) 询问。

然后注意到 k 特别小,不妨从这上面下手。

引理:若 abcd,则 aba+cb+dcd

证明很简单,从溶液浓度方面来理解:一杯甜度为 ab 的糖水兑上一杯甜度为 cd 的糖水,得到的糖水的甜度一定介于两者之间。

所以最优区间的长度一定小于 2k,否则可以分裂成两个合法区间,其中必然有一个不劣。

这时有用的区间就只剩下 O(nk) 个了。

可以建立 k 个 ST 表分别处理长度为 k2k1 的最优区间,但这样无法过掉 n106 的数据,会 MLE。

回想区间 dp 的做法,发现两者可以做一个平衡,将区间长度小于等于 2k 的区间用区间 dp 处理,这样状态数优化为 O(nk);区间长度大于 2k 的用 ST 表区间查询即可。

具体地,设 t=2k1,对于一组询问 [l,r],考虑所有的 lrt+1,以这些位置为左端点的有用区间一定被询问区间包含。可以对每个左端点预处理其的最优区间,然后使用 ST 表询问。这一部分复杂度为 O(nk+nlogn)

对于 l>rt+1,我们可以优化暴力的区间 dp。注意到所有在此时访问到的区间的长度均 <t,我们可以 O(nk) 地 dp。

时间复杂度为 (nk+nlogn+q)

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;
}

Day 4 T1

题目描述

菜汪酱是天文学家,喜欢观察行星的运动。

菜汪酱发现所在星系的行星都会绕着恒星转动。一共有 n 颗行星,分布在不同的轨道上。为了方便记录,菜汪酱把这些行星按照到恒星的距离依次标记为 1,2,,n

通过对行星运动规律的长时间总结和归纳,菜汪酱发现行星会在一个以恒星为圆点的正圆形轨道上顺时针转动。并且通过一系列的观察,菜汪酱得到了所有行星的周期。更具体地,菜汪酱发现第 i 颗行星的周期为 ai 个时间单位。由于某种神秘原因,菜汪酱发现所有 ai 互不相同。

你可能听说过 n+1 星连珠的说法。更具体地,如果存在一条直线使得恒星和 n 颗行星都在这一条直线上,那么就是认为这一时刻出现了 n+1 星连珠。也就是说,就算不是所有行星都在恒星的同一侧,只要在一条直线上,也认为出现了 n+1 星连珠。

菜汪酱认为出现这种情况是美丽的,因此想要知道发生 n+1 星连珠的时间。幸运的是,现在就已经出现了 n+1 星连珠,因此菜汪酱想要知道下一次在这一条直线上出现 n+1 星连珠是多少时间单位之后。

你只需要验算就可以了,所以只需要你输出对 998244353 取模的结果。

当然结果可能并非整数,但是可以证明结果一定是有理数,因此如果最后的答案是 p/qgcd(p,q)=1,那么你的输出 t 需要满足 0t<998244353t×qp(mod998244353)。题目保证答案存在且唯一。

输入格式

第一行一个整数 n

接下来一行 n 个整数 a1,,an

输出格式

一行一个整数表示答案。

样例输入

2
114 514

样例输出

14649

数据范围

对于 20% 的数据,n=2

对于 40% 的数据,n100,ai109

对于 100% 的数据,1n5000,1ai1018

思路:

很容易将原题面转化为:

给定 n 个正整数 {a1,a2,,an},求它们的最小公倍数的 12,结果对 998244353 取模。

首先要声明一点,不能在做 lcm 时顺便取模!

知道了这一点后,要么使用高精度算 gcd,要么就用接下来的优美方法。

鉴于高精度除法和取模太难写,在考场上很难写对,所以学习优美的另一种算法。

维护 lcm(a1,a2,,ai)=b1×b2××bi,当新加进来一个数 ai+1 时,推式子:

lcm(a1,a2,,ai+1)=lcm(lcm(a1,a2,,ai),ai+1)=lcm(b1×b2××bi,ai+1)=b1×b2××bi×ai+1gcd(b1×b2××bimodai+1,ai+1)

这里的 b1×b2××bimodai+1 每次都要计算,所以时间复杂度为 O(nlogV+n2),其中 V 表示值域。

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;
}
posted @   Brilliant11001  阅读(37)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
· 开源Multi-agent AI智能体框架aevatar.ai,欢迎大家贡献代码
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· AI技术革命,工作效率10个最佳AI工具
点击右上角即可分享
微信分享提示