文章目录
A:P11995 在小小的日历里面数呀数呀数(填空题)
题目描述
核心思路
这里我们很容易想到下一个完全平方年是 46^2,也就是 2116 年,直接枚举日期,判断日期是否合法,再统计有效日期,不过要注意从 20250330 开始枚举!
代码实现
#include <bits/stdc++.h>
using namespace std;
int days[] = {0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31};
// 判断一个日期是否有效
bool is(int date)
{
int year = date / 10000;
int month = date / 100 % 100;
int day = date % 100;
if (month == 0 || month > 12) return false;
if (month != 2 && (day == 0 || day > days[month])) return false;
if (month == 2)
{
int leap = year % 100 && year % 4 == 0 || year % 400 == 0;
if (day == 0 || day > days[2]+leap ) return false;
}
return true;
}
int main()
{
int res = 0;
for (int i = 20250330; i <= 21160101; i++)
if (is(i)) res++;
cout << res;
return 0;
}
B:P11996 我是黄色恐龙大将军(填空题)
题目描述
核心思路
计算 2^i
和 5^i
,然后提取这两个数的最高非零数字,把这两个数字相乘。把每次得到的乘积存入一个去重的集合中,确保每个乘积只算一次。最后把集合中所有不同的乘积加起来得到一个总和。因为是填空题,所以我们只要计算到某个合适的比较大的指数,之后结果就不再变化了。
代码实现
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
// 提取数字的最高非零位
int get(ll x)
{
while (x >= 10) x /= 10;
return x;
}
signed main()
{
set<int> res; // 存储去重后的结果
// 计算2^i和5^i的最高非零位的乘积
for (int i = 1; i <= 500000; i++)
{
ll x = pow(2, i);
ll y = pow(5, i);
res.insert(get(x) * get(y));
}
// 计算所有不同值的和
int sum = 0;
for (int i : res) sum += i;
cout << sum;
return 0;
}
C:P11997 化食欲为动力
题目描述
小蓝去超市买早餐,他需要选一个面包、一包火腿肠和一盒牛奶。每个商品都有一个食欲值,吃完这顿早餐后,他的动力值等于(面包的食欲 × 火腿肠的食欲) % 牛奶的食欲
。比如选第i
个面包(食欲a[i]
)、第j
包火腿肠(食欲b[j]
)和第u
盒牛奶(食欲c[u]
),动力值为 (a[i] × b[j]) % c[u]
。题目要求找出所有可能的组合中,小蓝能得到的最大动力值。
核心思路
通过三重循环暴力穷举所有组合,计算 (a[i] × b[j]) % c[u]
,然后记录最大的结果。
代码实现
#include <bits/stdc++.h>
using namespace std;
#define int long long
#define IOS ios_base::sync_with_stdio(0); cin.tie(0); cout.tie(0);
const int N = 205;
int n, m, k;
int a[N], b[N], c[N];
signed main() {
IOS;
cin >> n >> m >> k;
for (int i = 1; i <= n; i++) cin >> a[i];
for (int i = 1; i <= m; i++) cin >> b[i];
for (int i = 1; i <= k; i++) cin >> c[i];
int max_power = 0;
for (int i = 1; i <= n; i++)
for (int j = 1; j <= m; j++)
for (int u = 1; u <= k; u++)
max_power = max(max_power, (a[i] * b[j]) % c[u]);
cout << max_power << endl;
return 0;
}
D:P11998 哇,这就是 5p
题目描述
给定一个答题比赛,有 n 道题,你可以做对或做错。
- 做对:你可以得到 a[i] 分,概率是 p[i]。
- 做错:你得 0 分,概率是 (1 - p[i])。
我们要计算 最终总分是 m 的倍数的概率,并且 对 998244853 取模,防止数字过大。
🌟核心思路
如果暴力枚举所有得分组合,可能的得分组合太多,会超时。我们可以只关心得分除以 m 的余数,因为 只要余数是 0,就说明是 m 的倍数!
具体步骤:
- 输入处理:读取 n 和 m,然后读取 a 数组和 p 数组。对 a 数组中的每个元素取模 m,减少后续计算的复杂度。
- 初始化DP数组:创建一个大小为 m 的数组 f,初始时 f[0] = 1,其余为0。
- 遍历每一道题:
- 对于每道题i,创建一个临时数组dp,初始化为0。
- 计算做错的概率 q = (1 - p[i] + MOD) % MOD。
- 遍历当前所有可能的余数 j ,如果 f[j] 不为0,则分别处理做对和做错的情况,更新dp数组。
- 更新 f 数组:将 dp 数组复制到 f 数组中,处理下一道题。
- 输出结果:最终f[0]即为总分是m倍数的概率,输出其对MOD取模的值。
✅状态表示
我们用 f[j] 记录:
当前情况下,得分模 m 余数为 j 的概率。
初始状态:
- f[0] = 1(因为刚开始没做题,总分为 0,所以余数 0 的概率是 100%)。
- 其他 f[j] = 0(因为没做题,不可能有别的余数)。
✅状态计算
每道题有 两种情况:
- 做对了:
- 你的分数 增加 a[i],余数变成
(j + a[i]) % m
,概率要 乘上 p[i]。 - 更新公式:
dp[next] += f[j] * p[i]
- 你的分数 增加 a[i],余数变成
- 做错了:
- 你的分数 不变,余数还是 j,概率要 乘上 (1 - p[i])。
- 更新公式:
dp[j] += f[j] * (1 - p[i])
为了防止状态被覆盖,我们用一个 临时数组 dp[m] 来存储新的状态,计算完后再更新到 f[m]
里。这里注意题目中给出的 p[i] 是已经对 998244853 取模后的值。在代码中,做错题的概率是1-p[i]
,但由于 p[i]
是模后的值,直接计算 1 - p[i]
可能会导致负数,所以需要用 (1 - p[i] + MOD) % MOD
来确保结果非负。
📌代码实现
#include <bits/stdc++.h>
using namespace std;
#define int long long
#define IOS ios_base::sync_with_stdio(0); cin.tie(0); cout.tie(0)
const int N = 100010, MOD = 998244853;
int n, m;
int a[N], p[N];
signed main()
{
IOS;
cin >> n >> m;
for (int i = 0; i < n; i++) cin >> a[i], a[i] %= m; // 先取模,减少计算量
for (int i = 0; i < n; i++) cin >> p[i];
int f[m] = {0}; // f[j] 表示当前余数 j 的概率
f[0] = 1; // 初始状态总分为 0 的概率是 100%
for (int i = 0; i < n; i++) // 遍历每道题
{
int dp[m] = {0}; // 临时数组
int q = (1 - p[i] + MOD) % MOD; // 计算做错的概率,防止负数
for (int j = 0; j < m; j++)
{
if (f[j] == 0) continue; // 这个余数概率是 0,跳过计算
int next = (j + a[i]) % m; // 做对后的新余数
// 做对的情况:概率加到新余数
dp[next] = (dp[next] + f[j] * p[i]) % MOD;
// 做错的情况:概率加到原余数
dp[j] = (dp[j] + f[j] * q) % MOD;
}
memcpy(f, dp, sizeof dp); // 更新 f 数组,进行下一轮计算
}
cout << f[0] << endl; // 余数为 0 的概率,即答案
return 0;
}
时间复杂度
- 外层 n 道题,时间 O(n)
- 内层 m 个余数状态,时间 O(m)
- 总时间复杂度 O(n × m)
F:P12000 扶苏出勤日记
题目描述
扶苏每天都会赚到一定的钱,这些钱可以用来买游戏币。不过,游戏币的汇率每天都不同——有些天便宜,有些天贵。她希望每天都能玩相同局数的游戏,我们要帮她算出 最多每天能玩多少局。
分析一下问题:
a[i]
表示第i
天 1 块钱能买多少个游戏币。b[i]
表示第i
天扶苏能赚多少钱。- 她可以把钱存起来,也可以把游戏币存起来,不一定要当天就用掉。
- 目标: 让她每天都玩一样多的局数,并且这个局数尽量大。
🌟核心思路
我们可以用 二分答案 来试着找到这个“最多每天玩几局”的答案。
- 假设扶苏每天玩
ans
局游戏(每局要 1 个游戏币)。 - 能不能做到? 我们检查,如果
ans
局可行,那更小的ans
也一定可行(因为花的币更少了)。 - 检查
ans
是否可行?- 按天模拟,如果游戏币不够,就看看能不能用存下来的钱兑换到足够的游戏币。
- 兑换时,优先用最划算的钱来换,这样能换更多的游戏币。
代码逻辑:
-
建立一个队列
q
- 这个队列里存着 之前所有天赚的钱,并且按 汇率从高到低 排序。
- 因为我们希望优先使用 高汇率
a[i]
那天的钱,这样能换最多的游戏币。
-
每天赚钱的处理
- 新的一天到了,今天赚了
b[i]
块钱,汇率是a[i]
。 - 看看之前的钱,如果汇率比
a[i]
低,那就合并到今天的钱里(更划算)。 - 把这笔钱放进
q
,保证q
一直是按照汇率降序排列。
- 新的一天到了,今天赚了
-
每天消耗
ans
个游戏币s -= ans;
这是每天固定消耗的游戏币。- 如果游戏币不够了,就得想办法用存的钱换币。
-
怎么兑换游戏币?
- 直接从
q
最前面 开始兑换,因为q
里最前面的钱 汇率最高。 - 计算 最多能用这天的钱换多少游戏币,然后减少
need
(需要补的币)。 - 这天的钱如果用完了,就从
q
里删掉,继续从前面天数中a[i]
最大的一天开始兑换,直到满足 need。
- 直接从
-
兑换完后,如果
need > 0
,就返回false
- 即使按照最高效的策略兑换也无法满足这天玩
ans
局游戏的需求 - 说明我们尝试的
ans
取太大了,无法支撑。
- 即使按照最高效的策略兑换也无法满足这天玩
-
拥有游戏币的数量
s
- s 为负数说明缺币。
need = -s;
是我们欠缺的游戏币数量 s += max_p * q[0].a
是从负数开始一步步恢复的,如果ans
最终能被满足,那么s
也会累加成为正确的剩余游戏币数量
- s 为负数说明缺币。
📌核心代码
1. 主函数的二分查找
int l = 0, r = 1e18; // l 是最小可能值(0局),r 是最大可能值(设得很大)
int ans = 0;
while (l <= r) {
int mid = (l + r) >> 1; // 试试看每天玩 mid 局能不能成功
if (check(mid)) ans = mid, l = mid + 1; // 能行,看看还能不能更大
else r = mid - 1; // 不行,降低目标
}
这里我们用 二分法 试着找到每天最多能玩多少局。
mid
是当前尝试的局数。check(mid)
是核心函数,检查 如果每天玩mid
局,最终能不能撑住。
为什么右边界是 1e18
?
题目要求的是 最大每天游戏局数,我们必须确保 r
设得足够大,不会漏掉答案。
数据范围:
n
最多1e6
(100万天)b[i]
最大1e9
(每天最多赚 10 亿)a[i]
最大1000
(1 块钱最多买 1000 个币)
如果把所有的钱都换成游戏币,理论上的上限是:
最大总钱数 = 1e6 * 1e9 = 1e15
最大可能兑换币数 = 1e15 * 1e3 = 1e18
2. 检查 mid 是否可行
bool check(int ans) {
deque<Node> q; // 这个队列存每天的 a[i] 和 b[i],按照汇率 a[i] 降序排序
int s = 0; // 现有的游戏币数量
for (int i = 0; i < n; ++i)
{
// 今天新赚到的钱
Node cur = {a[i], b[i]}; // 今天的汇率 a[i],今天的收入 b[i]
// 把之前汇率比今天低的钱整合进今天的钱,确保队列是按“最高汇率”排序
while (!q.empty() && q.back().a <= cur.a)
{
cur.money += q.back().money;
q.pop_back();
}
q.push_back(cur); // 把今天的钱存进去
// 每天固定消耗 ans 个游戏币
s -= ans;
if (s < 0) // 如果币不够了,就想办法补足
{
int need = -s; // 需要补的币
while (need > 0 && !q.empty())
{
// 计算最多能从当前最高汇率的钱里兑换多少游戏币
// max_p是买游戏币花费的钱,向上取整确保满足need
int max_p = min(q[0].money, (need + q[0].a - 1) / q[0].a);
if (max_p == 0) break; // 兑换不了,直接失败
s += max_p * q[0].a; // 兑换后的币数
need -= max_p * q[0].a;
q[0].money -= max_p;
if (q[0].money == 0) q.pop_front(); // 这个天的钱用完了就删掉
}
if (need > 0) return false; // 还是不够,说明 d 取太大了
}
}
return true;
}
📌完整代码
#include <bits/stdc++.h>
using namespace std;
#define int long long
const int N = 1e6 + 10;
int n;
int a[N], b[N];
struct Node
{
int a, money; // 汇率和剩余钱数
};
bool check(int ans)
{
deque<Node> q;
int s = 0;
for (int i = 0; i < n; ++i)
{
Node cur = {a[i], b[i]};
while (!q.empty() && q.back().a <= cur.a)
{
cur.money += q.back().money;
q.pop_back();
}
q.push_back(cur);
s -= ans;
if (s < 0)
{
int need = -s;
while (need > 0 && !q.empty())
{
int max_p = min(q[0].money, (need + q[0].a - 1) / q[0].a);
if (max_p == 0) break;
s += max_p * q[0].a;
need -= max_p * q[0].a;
q[0].money -= max_p;
if (q[0].money == 0) q.pop_front();
}
if (need > 0) return false;
}
}
return true;
}
signed main()
{
ios_base::sync_with_stdio(false);
cin.tie(0);
int T;
cin >> T;
while (T--)
{
cin >> n;
for (int i = 0; i < n; i++) cin >> a[i];
for (int j = 0; j < n; j++) cin >> b[j];
int l = 0, r = 1e18;
int ans = 0;
while (l <= r)
{
int mid = (l + r) >> 1;
if (check(mid)) ans = mid, l = mid + 1;
else r = mid - 1;
}
cout << ans << endl;
}
return 0;
}
时间复杂度
- 二分法部分:最多
log2(1e18) ≈ 60
次。 - 每次
check(d)
运行时间:O(n)
(因为队列里的每个元素最多入队出队一次)。 - 总复杂度:
O(T * n log C)
(这里C = 1e18
)。