蓝桥杯算法
第一章 递推与递归
1.1 递归
所有递归都对应一棵递归搜索树
AcWing 820. 递归求斐波那契数列
#include<iostream>
using namespace std;
int Fib(int n)
{
if (n == 1 or n == 2) return 1;
else return Fib(n - 1) + Fib(n - 2);
}
int main()
{
int n;
cin >> n;
cout << Fib(n) << '\n';
return 0;
}
AcWing 92. 递归实现指数型枚举
递归(DFS)
参考代码
#include<iostream>
#include<vector>
using namespace std;
const int N = 16;
int n;
int st[N]; // s[i]记录当前位置数字的状态,0表示未考虑,1表示选择,2表示不选
void dfs(int u)
{
if (u > n) {
for (int i = 1; i <= n; i++) {
if (st[i] == 1) {
cout << i << ' ';
}
}
cout << '\n';
return;
}
st[u] = 2; // 第一个分支:不选
dfs(u + 1);
st[u] = 1; // 第二个分支:选
dfs(u + 1);
}
int main()
{
cin >> n;
dfs(1);
return 0;
}
AcWing 93. 递归实现组合型枚举
参考代码
// coding by xiongdx
#include<iostream>
using namespace std;
const int N = 15;
int n, m;
int way[N]; // n个位置
void dfs(int u, int start) // 当前递归到第u个位置,至少从start开始选
{
if (n - start + 1 < m - u + 1) return; // 剪枝:如果剩余的数字不够填满空位置,退出
if (u > m) { // 递归终点,输出答案
for (int i = 1; i <= m; i++) {
cout << way[i] << ' ';
}
cout << '\n';
return;
}
for (int i = start; i <= n; i++) { // 位置u依次放入 start~n 的每个数字
way[u] = i; // 第u个位置填入数字i
dfs(u + 1, i + 1); // 递归
//way[u] = 0; // 恢复现场
}
}
int main()
{
cin >> n >> m;
dfs(1, 1); // 递归起点
return 0;
}
AcWing 94. 递归实现排列型枚举
参考代码
#include<iostream>
using namespace std;
const int N = 10;
int n;
int state[N]; // 表示当前状态,0表示未放数,i表示放入数字i
bool used[N]; // 表示该数是否被用过
void dfs(int u)
{
if (u > n) { // 边界
for (int i = 1; i <= n; i++) cout << state[i] << ' '; // 打印方案
cout << '\n';
return;
}
// 依次枚举每个分支,即当前位置可以填哪些数
for (int i = 1; i <= n; i++) { // 枚举每一个数
if (!used[i]) { // 如果数字i未被使用过
state[u] = i; // 填入当前数字i
used[i] = true; // 表示数字i已经被用过了
dfs(u + 1); // 递归
// 恢复现场
//state[u] = 0;
used[i] = false;
}
}
}
int main()
{
cin >> n;
dfs(1);
return 0;
}
蓝桥杯真题:带分数
n = a + b / c,可推得 b = n * c - a * c
#include<iostream>
#include<cstring>
using namespace std;
const int N = 20;
int n;
int ans; // 统计满足条件的组合数
bool st[N], backup[N]; // st[N]:标记数字 1-9 是否已被使用。
// backup[N]:临时备份 st 的状态,用于检查 b 的合法性。
bool check(int a, int c) // 验证 a、b、c 是否满足条件
{
long long b = n * (long long)c - a * (long long)c; // n = a + b / c
if (!a || !b || !c) return false; // a,b,c 都不能是0
memcpy(backup, st, sizeof st);
// 检查 b 的每一位是否合法(无重复且不含 0)
while (b) {
int x = b % 10; // 每次取出末位
b /= 10; // 删除末位
if (!x || backup[x]) return false; // x不是0且x没有被用过
backup[x] = true; // 标记x被用过
}
for (int i = 1; i <= 9; i++) {
if (!backup[i]) {
return false; // 如果1~9存在某个数字没有出现
}
}
return true;
}
void dfs_c(int u, int a, int c)
{
if (u == 9) return; // 可行性剪枝:如果用完了9种数字,返回
if (check(a, c)) ans++; // 检查 a,b,c 是否满足要求
for (int i = 1; i <= 9; i++) { // 枚举c的数字组合
if (!st[i]) { // 如果没用过
st[i] = true; // 标记用过
dfs_c(u + 1, a, c * 10 + i); // 继续递归生成更长的c
st[i] = false; // 恢复现场
}
}
}
void dfs_a(int u, int a) // u表示已经用了多少数字
{
if (a >= n) return; // 可行性剪枝
if (a) dfs_c(u, a, 0); // 保证a不是0
for (int i = 1; i <= 9; i++) { // 枚举1~9数字
if (!st[i]) { // 如果数字i没有被用过
st[i] = true; // 标记用过
dfs_a(u + 1, a * 10 + i); // 继续递归
st[i] = false; // 恢复现场
}
}
}
int main()
{
cin >> n;
dfs_a(0, 0); // 递归起点
cout << ans << endl; // 输出答案
return 0;
}
1.2 递推
AcWing 717. 简单斐波那契
具体参考这里
参考代码
// Encoded by xiongdx
#include<iostream>
using namespace std;
int main()
{
int n;
cin >> n;
int f1 = 0, f2 = 1;
cout << f1 << ' '; // N > 0,所以肯定要输出第一项
n--; // 否则会多输出一项
while(n--) {
cout << f2 << ' '; // 每次输出新计算的一项
int t = f2;
f2 = f1 + f2; // 递推公式
f1 = t; // 滚动变量
}
return 0;
}
AcWing 95. 费解的开关
递归:将问题分解为相同的子问题,直到最简单的形式可以直接得出答案,再返回子问题的结果求得原问题答案
递推:先求出子问题,再用子问题求出原问题 (相当于递归的后半部分)
有题可推得:
-
开关的按动顺序不影响结果
-
每个格子只能按动1次或0次(因为按偶数次等于按0次,奇数次等于1次)
-
从上往下逐行按动开关,那么第i行开关是否按动被第i-1行的灯泡状态唯一确定
即如果第
(i-1, j)个灯泡是灭的,那么到第(i, j)个灯泡必须按动,否则第(i-1, j)个灯泡无法被点
因此,只需枚举第一行所以灯泡是否按动开关,即可递推出是否能在6步内点亮所以灯泡。
参考代码
#include<cstdio>
#include<cstring>
using namespace std;
const int N = 6;
char g[N][N], bg[N][N];
int dx[5] = {-1, 0, 1, 0, 0}, dy[5] = {0, 1, 0, -1, 0};
// 1. 每个开关最多会被按动一次(因为按动多次效果一样)
// 2. 最终的结果与开关按动的顺序无关(因为每个灯最终的状态只于被按几次有关系)
// 3. 从上往下逐行按动开关,那么第i行开关是否按动被第i-1行的灯泡状态唯一确定
void turn(int x, int y) //按动第x行第y列的开关
{
for (int i = 0; i < 5; i++){
int a = x + dx[i], b = y + dy[i];
if (a < 0 || a >= 5 || b < 0 || b >= 5) continue;
g[a][b] ^= 1;
}
}
int main()
{
int T;
scanf("%d", &T);
while (T--){
for (int i = 0; i < 5; i++) scanf("%s", bg[i]);
int res = 10;
for (int op = 0; op < 32; op++){ // 利用二进制枚举第一行的所有可能
int cnt = 0;
memcpy(g, bg, sizeof g); // 把二维字符数组bg的元素拷贝到g数组中进行操作
for (int i = 0; i < 5; i++){
if (op >> i & 1) { // 二进制op为1的位表示按动开关
turn(0, i);
cnt++;
}
}
// 第一行枚举固定,不断由上一行递推下一行的状态(即是0需要按,是1不需要按)
for (int i = 0; i < 4; i++){
for (int j = 0; j < 5; j++){
if (g[i][j] == '0'){
turn(i + 1, j); // g[i][j]为0,则需要按动下一行的开关
cnt++;
}
}
}
// 检查最后一行灯的状态(是否全亮)
bool success = true;
for (int i = 0; i < 5; i++){
if (g[4][i] == '0'){
success = false;
}
}
if (success && res > cnt) res = cnt;
}
if (res > 6) res = -1;
printf("%d\n", res);
}
return 0;
}
蓝桥杯真题:翻硬币
参考代码
#include<iostream>
using namespace std;
string s1, s2;
int n, ans;
int main()
{
cin >> s1 >> s2;
n = s1.size();
for (int i = 0; i < n; i++) {
if (s1[i] != s2[i]) { // 如果发现不相等,必定要翻动且只翻动一次
ans++; // 更新次数
if (i != n - 1) { // 实现翻硬币
if (s1[i + 1] == '*') s1[i + 1] = 'o';
else s1[i + 1] = '*';
}
}
}
cout << ans << endl; // 输出答案,数据保证一定有解
return 0;
}
AcWing 116. 飞行员兄弟
- 枚举所有方案
0 ~ 2^16 - 1 - 按照每种方案对所有灯泡进行操作
- 判断并记录成功方案
维护最小步数及其方案
保证输出步数最小的方案中字典序最小的方案:当两个方案步数相同(即二进制中的1的个数相同)时,优先枚举1更靠前(即从小到大)的方案。
#include<iostream>
#include<cstring>
#include<vector>
using namespace std;
typedef pair<int, int> PII;
const int N = 5;
char g[N][N], backup[N][N];
int get(int x, int y) // 求坐标(x, y)对应的 0~15 中的数字
{
return x * 4 + y;
}
void turn_one(int x, int y)
{
if (g[x][y] == '+') g[x][y] = '-';
else g[x][y] = '+';
}
void turn_all(int x, int y)
{
for (int i = 0; i < 4; i++) {
turn_one(x, i); // 按动一次第x行的所有开关
turn_one(i, y); // 按动一次第y列的所有开关
}
turn_one(x, y); // 多了一遍(x, y)的开关
}
int main()
{
for (int i = 0; i < 4; i++) cin >> g[i];
vector<PII> res;
for (int op = 0; op < 2 << 16; op++) { // 1. 枚举所有方案
vector<PII> temp;
memcpy(backup, g, sizeof g); // 备份当前状态
for (int i = 0; i < 4; i++) { // 行
for (int j = 0; j < 4; j++) { // 列
if (op >> get(i, j) & 1) { // 如果当前位为1,进行操作
temp.push_back({ i, j });
turn_all(i, j);
}
}
}
// 判断所有灯泡是否全亮
bool has_closed = false;
for (int i = 0; i < 4; i++) {
for (int j = 0; j < 4; j++) {
if (g[i][j] == '+') {
has_closed = true;
}
}
}
if (has_closed == false) {
if (res.empty() || res.size() > temp.size()) res = temp;
}
memcpy(g, backup, sizeof backup); // 还原备份
}
cout << res.size() << endl;
for (auto op : res) cout << op.first + 1 << ' ' << op.second + 1 << endl;
}
第二章 二分和前缀和
2.1 二分查找
整数二分步骤:
- 找到一个区间[L, R],使得答案一定在该区间中
- 找一个判断条件,使得该判断条件具有二段性,并且答案一定在二段性的分界点
- 分析中点M在该条件下是否成立,如果成立,考虑答案在哪个区间;如果不成立,考虑答案在哪个区间
- **如果数组左半部分满足check(),则 [l, r] 分为 [l, M-1] 和 [M,r],且其中M = (l + r + 1) / 2 **
- **否则如果数组右半部分满足check(),则 [l, r] 分为 [l, M] 和 [M+1,r],且其中M = (l + r) / 2 **
2.1.1 整数二分
AcWing 789. 数的范围
参考代码
#include<iostream>
using namespace std;
const int N = 100010;
int n, q;
int a[N];
void solve()
{
int k;
cin >> k;
int l = 0, r = n - 1;
while (l < r) { // 二分x的左端点
int mid = (l + r) / 2;
if (a[mid] >= k) r = mid;
else l = mid + 1;
}
if (a[l] == k) {
cout << l << ' '; // 输出左端点
r = n - 1;
while (l < r) { // 二分x的右端点
int mid = (l + r + 1) / 2;
if (a[mid] <= k) l = mid;
else r = mid - 1;
}
cout << r << endl; // 输出右端点
}
else cout << -1 << ' ' << -1 << '\n';
}
int main()
{
cin >> n >> q;
for (int i = 0; i < n; i++) cin >> a[i];
while (q--) {
solve();
}
}
蓝桥杯真题:机器人跳跃问题
关键:防止爆long long
参考代码
#include<iostream>
using namespace std;
typedef long long ll;
const int N = 100010;
ll n;
ll maxh = 0; // 记录最大的h
ll h[N];
bool check(ll E)
{
for (int i = 0; i < n; i++) {
E += E - h[i + 1];
if (E > maxh) return true; // 一定满足,返回以防止爆long long
if (E < 0) return false;
}
return true;
}
int main()
{
cin >> n;
for (int i = 1; i <= n; i++) {
cin >> h[i];
maxh = max(maxh, h[i]);
}
// 数据范围n <= 10^5, 二分检查是否满足条件时间复杂度为O(nlogn)
ll l = 0, r = 100000, mid = 0;
while (l < r) {
mid = (l + r) >> 1;
if (check(mid)) r = mid;
else l = mid + 1;
}
cout << l << endl;
return 0;
}
蓝桥杯真题:四平方和
解法一:暴力(蓝桥杯官网能过)
直接暴力枚举a, b, c的可能值,最后计算出$d = sqrt(n - a^2 - b^2 - c^2)$
时间复杂度:$O(n^{2/3})$
#include<iostream>
#include<algorithm>
#include<cstring>
#include<cmath>
using namespace std;
const int N = 500010;
int n;
int main()
{
cin >> n;
// 暴力枚举a, b, c所有可能的值,并求出d
for (int a = 0; a * a <= n; a++) {
for (int b = a; a * a + b * b <= n; b++) {
for (int c = b; a * a + b * b + c * c <= n; c++) {
int t = n - a * a - b * b - c * c;
int d = sqrt(t);
if (d * d == t) {
printf("%d %d %d %d\n", a, b, c, d);
return 0;
}
}
}
}
return 0;
}
解法二:暴力枚举+二分
空间换时间,先用O(n^2)时间复杂度把c^2 + d^2的所有可能值暴力枚举出并记录下来以及c和d的值,然后用O(n^2longn)的时间复杂度枚举a,b的可能值并二分求出对应的c^2 + d^2是否存在,并求出c和d
时间复杂度:$O(nlogn)$
#include<iostream>
#include<algorithm>
#include<cstring>
#include<cmath>
using namespace std;
const int N = 2500010;
struct Sum
{
int s, c, d;
bool operator < (const Sum& t) const // 重载小于号,保证字典序最小
{
if (s != t.s) return s < t.s;
if (c != t.c) return c < t.c;
return d < t.d;
}
}sum[N];
int n, m;
int main()
{
cin >> n;
for (int c = 0; c * c <= n; c++) {
for (int d = c; c * c + d * d <= n; d++) {
sum[m++] = { c * c + d * d, c, d }; // 先将c^2 + d^2计算出来
}
}
sort(sum, sum + m); // 排序, 保证单调性以二分
for (int a = 0; a * a <= n; a++) {
for (int b = a; a * a + b * b <= n; b++) {
int t = n - a * a - b * b; // 需要的c^2+d^2
int l = 0, r = m - 1;
while (l < r) { // 二分查找是否存在
int mid = l + r >> 1;
if (sum[mid].s >= t) r = mid;
else l = mid + 1;
}
if (sum[l].s == t) { // 如果存在,输出答案
printf("%d %d %d %d\n", a, b, sum[l].c, sum[l].d);
return 0;
}
}
}
return 0;
}
解法三:数组模拟哈希
与解法二类似,不同的是这里用哈希数组来存c^2 + d^2的值,维护哈希表hx[c^2+d^2] = c,通过c计算得出d。
时间复杂度:$O(n)$
#include<iostream>
#include<algorithm>
#include<cstring>
#include<cmath>
#include<unordered_map>
using namespace std;
const int N = 5000010;
int n;
int cd[N]; // 哈希数组存放c^2 + d^2以及对应的c
int main()
{
cin >> n;
memset(cd, -1, sizeof cd);
for (int c = 0; c * c <= n; c++) { // 将c^2+d^2所有可能的值打表用哈希数组记录
for (int d = c; c * c + d * d <= n; d++) {
int t = c * c + d * d;
if (cd[t] == -1) cd[t] = c; // 第一个即是字典序最小的
}
}
for (int a = 0; a * a <= n; a++) { // 暴力枚举a和b可能得值
for (int b = a; a * a + b * b <= n; b++) {
int t = n - a * a - b * b; // 求出 c^2 + d^2
if (cd[t] != -1) { // 如果存在
int c = cd[t];
int d = sqrt(t - c * c);// 根据c和c^2+d^2求出d
printf("%d %d %d %d\n", a, b, c, d); // 输出答案
return 0;
}
}
}
return 0;
}
蓝桥杯真题:分巧克力
参考代码
#include<iostream>
using namespace std;
typedef pair<int, int> PII;
typedef long long ll;
const int N = 100010;
int n, k;
int h[N], w[N];
bool check(int m)
{
ll cnt = 0; // 暴力枚举判断是否满足条件
for (int i = 0; i < n; i++) {
cnt += (h[i] / m) * (w[i] / m); // 注意括号!!!否则就不是下取整
}
return cnt >= k;
}
int main()
{
cin >> n >> k;
int l = 0, r = 0;
for (int i = 0; i < n; i++) {
cin >> h[i] >> w[i];
r = max(r, h[i]);
r = max(r, w[i]); // r取边长的最大值
}
while (l < r) { // 二分答案
int mid = (l + r + 1) >> 1;
if (check(mid)) l = mid;
else r = mid - 1;
}
cout << l << endl;
return 0;
}
1.3.2 实数二分:数的三次方根
#include<iostream>
using namespace std;
int main()
{
double x;
scanf("%lf", &x);
double l = -10000, r = 10000, mid = 0; // 避免小数时出错,比如0.001的答案是0.1,答案不在[0, 0.001]的范围内
while (r - l >= 1e-8) {
mid = (l + r) / 2;
if (mid * mid * mid >= x) r = mid;
else l = mid;
}
printf("%.6f", mid); // 保留6位小数
return 0;
}
2.2 前缀和
2.2.1 一维前缀和
算法思路:
S[i] = a[1] + a[2] + ... a[i]
Si = Si-1 + ai
建议下标从从1开始,令S0 = 0,这样求[1,10]就是S10 - S0 = S10
作用:快速求出任意区间的和
例如:求[l,r]范围内ai的和,可以用 Sr - Sl-1
a[l] + ... + a[r] = S[r] - S[l - 1]
如果询问下标从0开始:
a[l] + ... + a[r] = S[r + 1] - S[l ]
示例代码:
#include<iostream>
using namespace std;
const int N = 100010;
int n, m;
int a[N], s[N];
int main()
{
scanf_s("%d%d", &n, &m);
for (int i = 1; i <= n; i++) scanf_s("%d", &a[i]);
for (int i = 1; i <= n; i++) s[i] = s[i - 1] + a[i]; //前缀和的初始化
s[0] = 0;
while (m--)
{
int l, r;
scanf_s("%d%d", &l, &r);
printf("%d\n", s[r] - s[l - 1]); //区间和的计算
}
return 0;
}
蓝桥杯真题:k倍区间
前缀和
空间换时间:提前预处理出[L, R]中余数相同的个数
算法思路
- 前缀和性质:若两个前缀和
s[i]和s[j]的余数相同,则子数组i+1到j的和为k的倍数。 - 余数统计:通过
cnt数组记录各余数的出现次数,每遇到相同余数时,这些位置均可形成有效子数组。 - 初始化
cnt[0]:确保直接以当前元素结尾且和为k倍数的子数组被正确计数。
参考代码
#include<iostream>
#include<cstring>
#include<algorithm>
using namespace std;
typedef long long LL;
const int N = 100010;
int n, k;
LL s[N], cnt[N]; // s[i] 表示前i个元素的前缀和,cnt[i] 统计余数为i的前缀和出现的次数
int main()
{
scanf("%d%d", &n, &k);
for (int i = 1; i <= n; i++) {
scanf("%lld", &s[i]);
s[i] += s[i - 1]; // 计算前缀和
}
LL res = 0; // 存储答案
cnt[0] = 1; // 初始化前缀和是k的倍数的情况
for (int r = 1; r <= n; r++) {
res += cnt[s[r] % k]; // res加上与s[r]%k余数相同的前缀和的数量
cnt[s[r] % k]++; // s[r]%k 的余数加一
}
printf("%lld\n", res); // 输出答案
return 0;
}
2.2.2 二维前缀和
二维前缀和的计算公式:
$S[i][j] = S[i-1][j] + S[i][j-1] - S[i-1][j-1] + a[i][j]$
计算区间(x1, y1)到(x2, y2)的和:
$S[x2][y2] - S[x1 - 1][y2] - S[x2][y1 - 1] + S[x1 - 1][y1 - 1]$
AcWing 796.子矩阵的和
#include<iostream>
using namespace std;
const int N = 1010;
int n, m, q;
int s[N][N];
int main()
{
cin >> n >> m >> q;
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= m; j++) {
cin >> s[i][j];
s[i][j] += s[i - 1][j] + s[i][j - 1] - s[i - 1][j - 1];
}
}
while(q--) {
int x1, y1, x2, y2;
cin >> x1 >> y1 >> x2 >> y2;
cout << s[x2][y2] - s[x1 - 1][y2] - s[x2][y1 - 1] + s[x1 - 1][y1 - 1] << endl;
}
return 0;
}
AcWing 99. 激光炸弹
参考代码:
#include<bits/stdc++.h>
using namespace std;
using ll = long long;
using PII = pair<int, int>;
const int N = 5e3 + 10;
int n, m;
int s[N][N];
void solve()
{
int r;
cin >> n >> r;
r = min(r, 5001); // r 达到5001即可全部清除
for (int i = 0; i < n; i++) {
int x, y, w;
cin >> x >> y >> w;
x++, y++;
s[x][y] += w;
}
// 二维前缀和优化时间
for (int i = 1; i <= 5001; i++) {
for (int j = 1; j <= 5001; j++) {
s[i][j] += s[i - 1][j] + s[i][j - 1] - s[i - 1][j - 1]; // 这里只使用一个数组优化空间
}
}
// 遍历所有(R-1)×(R-1)的矩阵求出最大值,利用二维前缀和优化时间复杂度
int sums = 0;
for (int i = r; i <= 5001; i++) { // 枚举矩阵的右下角(i, j)
for (int j = r; j <= 5001; j++) {
sums = max(sums, s[i][j] - s[i - r][j] - s[i][j - r] + s[i - r][j - r]);
}
}
cout << sums << endl;
}
int main()
{
cin.tie(0);
cout.tie(0);
ios::sync_with_stdio(false);
int t = 1;
// cin >> t;
while (t--)
solve();
return 0;
}
// Created by xiongdx on 9/9/2024
第三章 数学与简单DP
3.1 数学知识
AcWing1205. 买不到的数目
打表找规律
n m ans
3 2 1
3 4 5
3 5 7
3 7 11
3 8 13
3 10 17
n m ans
5 2 3
5 3 7
5 4 11
5 6 19
5 7 23
5 8 27
5 9 31
得出规律:
当n==3时,ans = 2*m - 3
当n==5时,ans = 4*m - 5
即当n确定时,ans = (n - 1) * m - n
又显然n和m等价,故
$ans = nm - n - m = (n - 1)(m - 1) - 1$
代入验证可知正确。
结论
如果$a, b$是正整数且互质,那么由$ax+by, x >= 0, y >= 0$不能凑出的最大整数是$ab - a - b$
具体证明可参考这里
参考代码
#include <cstdio>
#include <cstring>
#include <iostream>
#include <algorithm>
using namespace std;
typedef long long LL;
LL n, m;
int main()
{
cin >> n >> m;
LL ans = n * m - n - m;
cout << ans << endl;
return 0;
}
暴力枚举(TLE)
#include<iostream>
#include<cstring>
#include<algorithm>
using namespace std;
const int N = 1010;
int n, m;
int a[N], cnt;
bool dfs(int x)
{
if (x == 0) return true; // 如果x被减到0,说明可以凑出来
if (x >= n && dfs(x - n)) return true; // 用n凑
if (x >= m && dfs(x - m)) return true; // 用m凑
return false; // 都凑不出来
}
int main()
{
cin >> n >> m;
int res = 0;
for (int i = 1; i <= 1000; i++) {
if (!dfs(i)) res = i;
}
cout << res << endl;
return 0;
}
AcWing 1211. 蚂蚁感冒
关键:蚂蚁相遇等价于蚂蚁互相穿过对方
算法思路:
先统计被感染的蚂蚁左边且向右走的蚂蚁数量lr,再统计被感染的蚂蚁右边且向左走的蚂蚁数量rl
-
如果被感染的蚂蚁是向左走的,并且没有在它左边且向右走的蚂蚁,或者被感染的蚂蚁是向右走的,并且在它右边且向左走的蚂蚁
那么,只有初始的一个蚂蚁感冒
-
否则如果被感染的蚂蚁前往的方向上存在“逆行”的蚂蚁
那么所有“逆向”的蚂蚁都会被感染,加上初始的一个蚂蚁,答案是
lr + rl + 1
参考代码
#include<iostream>
#include<cstring>
#include<algorithm>
using namespace std;
const int N = 55;
int n;
int a[N];
int main()
{
cin >> n;
for (int i = 0; i < n; i++) cin >> a[i];
int lr = 0, rl = 0; // 分别表示左边向右走的蚂蚁数量和右边向左走的蚂蚁数量
for (int i = 1; i < n; i++) {
if (abs(a[i]) < abs(a[0]) && a[i] > 0) lr++;
else if (abs(a[i]) > abs(a[0]) && a[i] < 0) rl++;
}
if (a[0] > 0 && rl == 0 || a[0] < 0 && lr == 0) cout << 1 << endl; // 只有最初的一个蚂蚁感冒
else cout << lr + rl + 1 << endl; // 会把所有反着走的都感染
return 0;
}
3.2 动态规划
蓝桥杯真题:地宫取宝
递归解法
2.1 思路
当走到某个格子上的时候:
-
如果格子上宝贝的价值大于已有宝贝的最大值,那么可以选择拿或者不拿。
-
如果格子上宝贝的价值小于或者等于已有宝贝的最大值,那么只能选择不拿。
-
必须从左上角走到右下角,且只要到达右下角时物品个数满足条件即算一种方案。
-
只能选择向下或者向右走
-
不是必须到出口时,宝贝数量恰好满足条件,而是可以在任意位置就宝贝数量就可以满足条件,只需保证到达出口时宝贝数量仍然满足条件即可
参考代码
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
typedef long long LL;
const int N = 55, MOD = 1e9 + 7;
int n, m, k;
int w[N][N]; // 存储宝物价值
int dp[N][N][13][14]; // f[i][j][k][w] 从起点走到(i, j)且已经取了k件物品且最后一件物品的价值是w的合法方案的集合(个数)
int dx[2] = { 1, 0 };
int dy[2] = { 0, 1 };
// 分别表示当前搜到的坐标(x, y),当前已经拿的宝物数量cnt,当前宝物的最大值mx
LL dfs(int x, int y, int cnt, int mx)
{
if (x == n && y == m) return cnt == k ? 1 : 0; // 搜到终点时返回,宝物个数等于k时返回1,否则为0
if (dp[x][y][cnt][mx] != -1) return dp[x][y][cnt][mx]; // 计算过久直接返回结果
LL res = 0; // 记录最大方案数
for (int i = 0; i < 2; i++) { // 枚举两个方向
int nx = x + dx[i], ny = y + dy[i];
if (nx < 1 || nx > n || ny < 1 || ny > m) continue; // 越界
if (w[nx][ny] > mx && cnt < k) res = (res + dfs(nx, ny, cnt + 1, w[nx][ny])) % MOD; // 拿
res = (res + dfs(nx, ny, cnt, mx)) % MOD;
}
dp[x][y][cnt][mx] = res; // 记忆化搜索,存储结果避免重复计算
return res;
}
int main()
{
memset(dp, -1, sizeof dp); // 初始化dp为-1,判断是否已经被更新
cin >> n >> m >> k;
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= m; j++) {
cin >> w[i][j];
w[i][j]++; // 让所有价值都加一,避免第一个物品可能是0而导致无法取到
}
}
LL ans = (dfs(1, 1, 0, 0) + dfs(1, 1, 1, w[1][1])) % MOD; // 对第一个宝物拿或不拿两种情况分别dfs
cout << ans << endl; // 输出答案
return 0;
}
动态规划
摘花生+最长上升子序列 求方案数 四维dp
参考代码
#include<iostream>
#include<cstring>
#include<algorithm>
using namespace std;
const int N = 55, MOD = 1e9 + 7;
int n, m, k;
int w[N][N];
int f[N][N][13][14]; // f[i][j][k][w] 从起点走到(i, j)且已经取了k件物品且最后一件物品的价值是w的合法方案的集合(个数)
int main()
{
cin >> n >> m >> k;
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= m; j++) {
cin >> w[i][j];
w[i][j]++; // 所有值加一防止数组越界
}
}
f[1][1][1][w[1][1]] = 1; // 选第一件物品
f[1][1][0][0] = 1; // 不选第一件物品
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= m; j++) { // 枚举坐标(i, j)
if (i == 1 && j == 1) continue;
for (int u = 0; u <= k; u++) { // 枚举已选物品的个数
for (int v = 0; v <= 13; v++) { // 枚举最后一个物品的价值
// 不选
f[i][j][u][v] = (f[i][j][u][v] + f[i - 1][j][u][v]) % MOD;
f[i][j][u][v] = (f[i][j][u][v] + f[i][j - 1][u][v]) % MOD;
if (u > 0 && v == w[i][j]) { // 选
for (int c = 0; c < v; c++) {
f[i][j][u][v] = (f[i][j][u][v] + f[i - 1][j][u - 1][c]) % MOD;
f[i][j][u][v] = (f[i][j][u][v] + f[i][j - 1][u - 1][c]) % MOD;
}
}
}
}
}
}
int res = 0; // 统计最大的方案数
for (int i = 0; i <= 13; i++) res = (res + f[n][m][k][i]) % MOD;
cout << res << endl;
return 0;
}
蓝桥杯真题:波动数列
- 所有满足条件的$(n-1)d_1 + (n-2)d_2 + ... + d_{n-1}$ 与 $s$ 模 $n$ 的余数相同
闫氏DP分析法(类似背包问题??)
参考代码
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 1010, MOD = 1e8 + 7;
int f[N][N]; // 前i项中,当前在总和模n的余数是j的方案的集合(总数)
// 计算 a 除以 b 的正余数
int get_mod(int a, int b)
{
return (a % b + b) % b;
}
int main()
{
int n, s, a, b;
cin >> n >> s >> a >> b;
f[0][0] = 1;
for (int i = 1; i < n; i++) { // 前 n - 1 项
for (int j = 0; j < n; j++) { // 模n的余数 0 ~ n - 1
f[i][j] = (f[i - 1][get_mod(j - a * (n - i), n)] + f[i - 1][get_mod(j + b * (n - i), n)]) % MOD;
}
}
cout << f[n - 1][get_mod(s, n)] << endl; // 避免s为负时模n数组越界
return 0;
}
第四章 枚举、模拟与排序
4.1 枚举
蓝桥杯真题:连号区间数
连号区间的极差等于区间长度-1
参考代码
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 10000;
int n;
int a[N];
int main()
{
cin >> n;
for (int i = 1; i <= n; i++) {
cin >> a[i];
}
int cnt = 0; // 连号区间数量
for (int i = 1; i <= n; i++) {
int mx = a[i], mn = a[i]; // 最大值和最小值
for (int j = i; j <= n; j++) {
mx = max(mx, a[j]);
mn = min(mn, a[j]);
// 如果区间的极差等于区间长度-1,则为连号区间
if ((mx - mn) == (j - i)) cnt++;
}
}
cout << cnt << endl;
return 0;
}
蓝桥杯真题:递增三元组
解法一:二分查找
数据范围:$1 ≤ N ≤ 10^5$,故时间复杂度应该在$O(n longn)$,即只能枚举一个数组,因为数组B与数组A和数组C都存在直接关系,故应枚举数组B,然后二分判断数组A中有多少元素小于B[j](数组A中第一个小于B[j]的元素下标为i),再二分判断数组C中有多少元素大于B[j](数组C中第一个小于B[j]的元素下标为k),最后cnt += (LL)(i + 1) * (LL) (n - k)
参考代码
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
typedef long long LL;
const int N = 100010;
int n;
int a[N], b[N], c[N];
int main()
{
scanf("%d", &n);
for (int i = 0; i < n; i++) scanf("%d", &a[i]);
for (int i = 0; i < n; i++) scanf("%d", &b[i]);
for (int i = 0; i < n; i++) scanf("%d", &c[i]);
sort(a, a + n);
//sort(b, b + n); // 数组B不用排序
sort(c, c + n);
LL cnt = 0;
for (int j = 0; j < n; j++) { // 枚举数组b,a和c都与b有联系
int l = 0, r = n - 1;
while (l < r) { // 二分数组A
int mid = l + r + 1 >> 1;
if (a[mid] < b[j]) l = mid;
else r = mid - 1;
}
int i = l; // 第一个小于B[j]的下标
l = 0, r = n - 1;
while (l < r) { // 二分数组C
int mid = l + r >> 1;
if (c[mid] > b[j]) r = mid;
else l = mid + 1;
}
int k = l; // 第一个大于B[j]的下标
// 判断结果是否符合(可能存在C数组中所以元素都小于B[j]的情况)
if (a[i] < b[j] && b[j] < c[k]) cnt += (LL)(i + 1) * (LL)(n - k);
}
printf("%lld\n", cnt);
return 0;
}
解法二:前缀和
同上,枚举数组$B$的每个元素,对于每个元素$B_j$,求
-
在数组$A$中有多少个元素小于$B_j$
-
在数组$C$中有多少个元素大于$B_j$
首先维护一个数组$cnt[i]$,表示在数组$A$中元素$i$出现的次数cnt[A[i]]++,然后求数组$cnt[i]$的前缀和$s[i]$,表示在数组$A$中元素$0-i$出现的次数。则:
- 在数组$A$中小于$B_j$ 的元素个数 = $s[B_j - 1]$
- 在数组$C$中大于$B_j$ 的元素个数 = $s[N] - s[B_j]$
则总个数cnt += s[B[j] - 1] * s[N] - s[B[j]];
参考代码
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
typedef long long LL;
const int N = 100010;
int n;
int cntA[N], cntC[N]; // 分别表示在数组A和数组C中元素i出现的次数
int B[N]; // 存储数组B
LL sA[N], sC[N]; // 分别表示在数组A和数组C中元素0-i出现的次数
int main()
{
scanf("%d", &n);
// 读入数据并统计次数
for (int i = 1; i <= n; i++) {
int t;
scanf("%d", &t);
cntA[++t]++; // 因为数据从0开始,前缀和需要从1开始,故所有数据加一
}
for (int j = 1; j <= n; j++) scanf("%lld", &B[j]), B[j]++;
for (int k = 1; k <= n; k++) {
int t;
scanf("%d", &t);
cntC[++t]++;
}
// 计算前缀和
for (int i = 1; i <= N; i++) sA[i] = sA[i - 1] + cntA[i];
for (int k = 1; k <= N; k++) sC[k] = sC[k - 1] + cntC[k];
// 计算递增三元组
LL cnt = 0; // 存储答案
for (int j = 1; j <= n; j++) { // 枚举数组B
LL a = sA[B[j] - 1], c = sC[N] - sC[B[j]]; // 计算个数
cnt += a * c;
}
printf("%lld\n", cnt); // 输出答案
return 0;
}
蓝桥杯真题:特别数的和
暴力枚举每一个数,把每个数的数位取出来单独判断即可
常用小技巧:
- 取出x的每位数字
int x = 2019;
string str;
while(x) {
int t = x % 10;
x /= 10;
}
- 将字符数字转为数字
string str = "2019";
int x = 0;
for (int i = 0; i < str.size(); i ++ )
x = x * 10 + str[i] - '0';
参考代码
#include<iostream>
using namespace std;
int n;
bool check(int x)
{
while(x){
int t = x % 10;
if (t == 2 || t == 0 || t == 1 || t == 9) return true;
x /= 10;
}
return false;
}
int main()
{
cin >> n;
int ans = 0;
for (int i = 1; i <= n; i++) {
if (check(i)) ans += i;
}
cout << ans << endl;
return 0;
}
蓝桥杯真题:错误票据
读入一行数据使用getline(cin, str)
注意:getline(cin, str)会把结尾的换行也读入!具体见下面代码第17行
时间复杂度:$O(n)$
参考代码
#include <bits/stdc++.h>
using namespace std;
const int N = 100010;
int n;
int a[N];
int cnt[N];
int main()
{
int t = 0;
cin >> t;
string line;
getline(cin, line); // 忽略第一行的回车
while (t--) {
getline(cin, line); // 从标准输入读取一行字符串,存储到 line 中
stringstream ssin(line); // 将 line 包装成一个字符串流 ssin,方便逐个提取其中的整数
// 从字符串流 ssin 中逐个提取整数,存储到数组 a 中,并更新 n(数组 a 的当前元素个数)
while (ssin >> a[n]) {
cnt[a[n]]++; // 统计每个数字出现的次数
n++;
}
}
int less = 0, more = 0, flag = 0; // 分别表示缺号、重号、是否都已找到
int start = 1; // 记录起点
while (cnt[start] == 0) start++;
for (int i = start; i <= 100000; i++) {
if (cnt[i] == 0) less = i, flag++; // 缺号
if (cnt[i] == 2) more = i, flag++; // 重号
if (flag == 2) break; // 如果两个都找到,返回
}
cout << less << ' ' << more << endl; // 输出答案
return 0;
}
// Encoding by xiongdx.
蓝桥杯真题:回文日期1
日期类问题+字符串处理
参考代码
#include <iostream>
#include <bits/stdc++.h>
using namespace std;
void f() { exit(0); }
int n;
int dn[13] = { 0, 31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 };
int check(string s)
{
int sn = s.size();
// 判断是否是回文串
for (int i = 0, j = sn - 1; i <= j; i++, j--) {
if (s[i] != s[j]) return 0;
}
// 判断是否是ABABBABA型
if (s[0] != s[1] && s[0] == s[2] && s[1] == s[3]) return 2;
return 1;
}
int main()
{
cin >> n;
int dd = n % 100;
n /= 100;
int mm = n % 100;
int yy = n / 100;
bool f1 = false, f2 = false;
// 先把题目给的从 dd+1 天开始的月循环完
for (int d = dd + 1; d <= dn[mm]; d++) {
string s = "";
s += to_string(yy);
if (mm < 10) s += '0';
s += to_string(mm);
if (d < 10) s += '0';
s += to_string(d);
int res = check(s);
if (!f1 && res >= 1) {
f1 = true;
cout << s << endl;
}
if (!f2 && res == 2) {
f2 = true;
cout << s << endl;
}
if (f1 && f2) return 0;
}
// 再把题目中给的从 mm+1 月开始的年循环完
for (int m = mm + 1; m <= 12; m++) {
for (int d = 1; d <= dn[m]; d++) {
string s = "";
s += to_string(yy);
if (m < 10) s += '0';
s += to_string(m);
if (d < 10) s += '0';
s += to_string(d);
int res = check(s);
if (!f1 && res >= 1) {
f1 = true;
cout << s << endl;
}
if (!f2 && res == 2) {
f2 = true;
cout << s << endl;
}
if (f1 && f2) return 0;
}
}
// 最后从 yy+1 年开始循环
for (int y = yy + 1; y <= 9999; y++) {
if (y % 4 == 0 && y % 100 != 0 || y % 400 == 0) dn[2] = 29;
else dn[2] = 28;
for (int m = 1; m <= 12; m++) {
for (int d = 1; d <= dn[m]; d++) {
string s = ""; // 将数字转换为字符串
s += to_string(y);
if (m < 10) s += '0';
s += to_string(m);
if (d < 10) s += '0';
s += to_string(d);
int res = check(s);
if (!f1 && res >= 1) {
f1 = true;
cout << s << endl;
}
if (!f2 && res == 2) {
f2 = true;
cout << s << endl;
}
if (f1 && f2) return 0;
}
}
}
return 0;
}
蓝桥杯真题:回文日期2
- 枚举回文数
- 判断是否在范围内
- 再判断日期是否合法
整数构造回文串的方法
参考代码
#include<iostream>
#include<cstring>
#include<algorithm>
using namespace std;
int days[13] = { 0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 };
bool check(int date)
{
int day = date % 100;
int month = date / 100 % 100;
int year = date / 10000;
if (year % 100 && year % 4 == 0 || year % 400 == 0) days[2] = 29;
else days[2] = 28; // 判断平年还是闰年
if (month == 0 || month > 12 || day == 0 || day > days[month]) return false;
return true;
}
int main()
{
int date1 = 0, date2 = 0;
cin >> date1 >> date2;
int res = 0;
for (int i = 1000; i < 10000; i++) {
int date = i, x = i;
// 将i反转并合并到date中,构造回文串
for (int j = 0; j < 4; j++) date = date * 10 + x % 10, x /= 10;
if (date1 <= date && date <= date2 && check(date)) res++;
}
cout << res << endl;
return 0;
}
蓝桥杯真题:日期问题
scanf()的格式化输入
printf()的格式化输出
参考代码
#include<iostream>
#include<string>
#include<set>
#include<algorithm>
using namespace std;
const int N = 100010;
set<string> s;
int days[13] = { 0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 };
void check(int y, int m, int d)
{
if (y >= 60) y += 1900;
else y += 2000;
if (y % 100 && y % 4 == 0 || y % 400 == 0) days[2] = 29;
else days[2] = 28;
if (m != 0 && m <= 12 && d != 0 && d <= days[m]) {
string t = "";
t += to_string(y);
t += '-';
if (m < 10) t += '0';
t += to_string(m);
t += '-';
if (d < 10) t += '0';
t += to_string(d);
s.insert(t);
}
}
int main()
{
int a, b, c;
scanf("%d/%d/%d", &a, &b, &c);
// 年 月 日
check(a, b, c);
// 月 日 年
check(c, b, a);
// 日 月 年
check(c, a, b);
for (auto x : s) {
cout << x << endl;
}
return 0;
}
参考代码2
#include<iostream>
#include<algorithm>
using namespace std;
const int N = 100010;
int days[13] = { 0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 };
bool check(int year, int month, int day)
{
if (year % 100 && year % 4 == 0 || year % 400 == 0) days[2] = 29;
else days[2] = 28;
if (month == 0 || month > 12 || day == 0 || day > days[month]) return false;
return true;
}
int main()
{
int a, b, c;
scanf("%d/%d/%d", &a, &b, &c);
// 枚举所有日期并判断是否合法
for (int date = 19600101; date <= 20591231; date++) {
int year = date / 10000, month = date / 100 % 100, day = date % 100;
if (check(year, month, day)) {
if (year % 100 == a && month == b && day == c ||
month == a && day == b && year % 100 == c ||
day == a && month == b && year % 100 == c )
printf("%d-%02d-%02d\n", year, month, day); // "%02d"位数不足2位补0
}
}
return 0;
}
蓝桥杯真题:航班时间
sscanf(str.c_str, "%d:%d:%d %d:%d:%d (+%d)", &h1, &m1, &s1, &h2, &m2, &s2, &d);,具体见25行
参考代码
#include<iostream>
#include<string>
#include<cstring>
#include<algorithm>
using namespace std;
const int N = 100010;
int n;
int getSecond(int h, int m, int s) // 将 时:分:秒 转换为 秒数
{
return h * 3600 + m * 60 + s;
}
int getTime() // 读入时间并求出往返的时间差
{
string line;
getline(cin, line);
if (line.back() != ')') line += " (+0)"; // 统一格式
int h1, m1, s1, h2, m2, s2, d;
sscanf(line.c_str(), "%d:%d:%d %d:%d:%d (+%d)", &h1, &m1, &s1, &h2, &m2, &s2, &d);
return getSecond(h2, m2, s2) - getSecond(h1, m1, s1) + d * 24 * 3600;
}
int main()
{
scanf("%d", &n);
string line;
getline(cin, line); // 忽略第一行末尾的回车
while (n--) {
int time = (getTime() + getTime()) / 2;
int hour = time / 3600, minute = time % 3600 / 60, second = time % 60;
printf("%02d:%02d:%02d\n", hour, minute, second);
}
return 0;
}
scanf()格式化输入,具体见第13行
#include<iostream>
using namespace std;
int getSecond(int h, int m, int s) // 将 时:分:秒 转换为 秒数
{
return h * 3600 + m * 60 + s;
}
int getTime() // 读入时间并求出往返的时间差
{
int h1,m1,s1,h2,m2,s2,d=0;
scanf("%d:%d:%d %d:%d:%d (+%d)",&h1,&m1,&s1,&h2,&m2,&s2,&d);
int time = getSecond(h2, m2, s2) - getSecond(h1, m1, s1) + d * 24 * 3600;
return time;
}
int main()
{
int t;
cin>>t;
for(int i=0;i<t;i++)
{
int time = (getTime() + getTime()) / 2;
printf("%02d:%02d:%02d\n",time / 3600, time / 60 % 60, time % 60);
}
}
4.2 排序
蓝桥杯真题:外卖店优先级
模拟 + 排序
参考代码
#include<iostream>
#include<cstring>
#include<algorithm>
using namespace std;
typedef pair<int, int> PII;
const int N = 100010;
int n, m, T;
int score[N], last[N]; // 分别表示第i个店铺的优先级和上一次有订单的时间
bool st[N]; // 记录店铺是否在优先缓存中
PII order[N];
int main()
{
scanf("%d%d%d", &n, &m, &T);
for (int i = 0; i < m; i++) scanf("%d%d", &order[i].first, &order[i].second);
sort(order, order + m); // 先按时间顺序,再按店铺id顺序排序
for (int i = 0; i < m;) {
// 处理t时刻之前的订单
int j = i; // 处理同一时刻同一店铺的cnt个订单
while(j < m && order[j] == order[i]) j++;
int t = order[i].first, id = order[i].second, cnt = j - i;
i = j;
score[id] -= t - last[id] - 1;
if (score[id] < 0) score[id] = 0;
if (score[id] <= 3) st[id] = false;
// 处理t时刻的订单
score[id] += cnt * 2;
if (score[id] > 5) st[id] = true;
last[id] = t; // 更新last
}
// 处理每个店铺直到最后的T时刻没有订单的情况
for (int i = 1; i <= n; i++) {
score[i] -= T - last[i];
if (score[i] <= 3) st[i] = false;
}
// 遍历优先级统计出答案
int res = 0;
for (int i = 1; i <= n; i++) {
res += st[i];
}
printf("%d\n", res);
}
第五章 树状数组与线段树
第六章 BFS与图论
6.1 双指针
蓝桥杯真题:日志统计
用pair存储数据{ id, ts },然后按id和时间排序,用双指针i和j指向区间$[T, T + D)$,判断i和j - 1是否是同一id且相差时间是否小于D,然后i和j同时向右移动判断。
时间复杂度:$O(N)$
参考代码
#include<iostream>
#include<vector>
#include<cstring>
#include<algorithm>
using namespace std;
typedef pair<int, int> PII;
const int N = 100010;
int n, d, k;
PII Log[N]; // 存储日志信息 { id, ts }
vector<int> hot; // 存储热帖
int main()
{
scanf("%d%d%d", &n, &d, &k);
for (int i = 0; i < n; i++) {
int ts, id;
scanf("%d%d", &ts, &id);
Log[i] = { id, ts };
}
sort(Log, Log + n); // 先按帖子排序,再按时间排序
int cnt = 0;
for (int i = 0, j = k - 1; i < n, j < n; ) { // 找k个日志
// 判断是否是同一个id并且相差时间是否小于D
if (Log[i].first == Log[j].first && Log[j].second - Log[i].second < d) {
hot.push_back(Log[i].first); // 放入热帖列表
i = j; // 寻找下一个帖子
while (i < n && Log[i].first == Log[j].first) i++;
j = i + k - 1;
}
else {
i ++;
j ++;
}
}
// 输出热帖id
for (int i = 0; i < hot.size(); i++) cout << hot[i] << endl;
return 0;
}
蓝桥杯真题:完全二叉树的权值
简单题,一发AC
时间复杂度:$O(N)$
#include<cstring>
#include<iostream>
#include<algorithm>
using namespace std;
typedef long long ll;
const int N = 100010;
int n;
int main()
{
scanf("%d", &n);
int i = 1, h = 1, cnt = 1;
ll sums = -100000000000;
int ans = 1;
while (i <= n) {
ll s = 0;
for (int j = 0; j < cnt; j++) {
int t;
scanf("%d", &t);
s += t;
i++;
if (i > n) break;
}
if (s > sums) sums = s, ans = h;
cnt *= 2;
h++;
}
printf("%d\n", ans);
return 0;
}
6.2 BFS
BFS模板
AcWing 1101. 献给阿尔吉侬的花束
简单BFS求最短路
参考代码
#include<iostream>
#include<queue>
#include<cstring>
#include<algorithm>
using namespace std;
typedef pair<int, int> PII;
const int N = 210;
int n, m;
char g[N][N];
int dist[N][N];
int dx[4] = { -1, 0, 1, 0 }, dy[4] = { 0, 1, 0, -1 }; // 上右下左
bool bfs(int a, int b)
{
queue<PII> q;
// 初始化
dist[a][b] = 0;
q.push({ a, b });
while (!q.empty()) {
PII t = q.front();
q.pop();
for (int i = 0; i < 4; i++) {
int x = t.first + dx[i], y = t.second + dy[i];
if (x >= 0 && x < n && y >= 0 && y < m && dist[x][y] == -1 && g[x][y] != '#') {
dist[x][y] = dist[t.first][t.second] + 1;
q.push({ x, y });
if (g[x][y] == 'E') return true; // 找到奶酪
}
}
}
return false;
}
int main()
{
int t;
cin >> t;
while (t--) {
memset(dist, -1, sizeof dist);
scanf("%d%d", &n, &m);
int x = 0, y = 0; // 记录老鼠的初始位置
int xe = 0, ye = 0; // 记录奶酪的位置
for (int i = 0; i < n; i++) scanf("%s", g[i]);
for (int i = 0; i < n; i++) {
for (int j = 0; j < m; j++) {
if (g[i][j] == 'S') x = i, y = j;
if (g[i][j] == 'E') xe = i, ye = j;
}
}
if (bfs(x, y)) cout << dist[xe][ye] << endl;
else cout << "oop!" << endl;
}
return 0;
}
AcWing 1096. 地牢大师
简单题,BFS + 三维数组,套路跟二维数组相同,注意每次需要清空队列!可以在bfs()内创建队列。
另外对于三元数据的存储,我这里使用的是嵌套pairpair<int, pair<int, int>>存储的,更建议使用struct结构体存储。
参考代码1(嵌套pair)
#include<cstring>
#include<iostream>
#include<algorithm>
#include<queue>
using namespace std;
typedef long long ll;
typedef pair<int, pair<int, int>> PIII;
const int N = 110;
int l, r, c;
char g[N][N][N];
int dist[N][N][N];
int dx[6] = { -1, 0, 1, 0, 0, 0 }, dy[6] = { 0, 1, 0, -1, 0, 0 }, dz[6] = { 0, 0, 0, 0, 1, -1 };
void bfs(int x, int y, int z)
{
queue<PIII> q;
dist[z][x][y] = 0;
q.push({ z, {x, y } });
while (!q.empty()) {
auto t = q.front(); q.pop();
int tz = t.first, tx = t.second.first, ty = t.second.second;
for (int i = 0; i < 6; i++) {
int z = tz + dz[i], x = tx + dx[i], y = ty + dy[i];
if (z >= 0 && z < l && x >= 0 && x < r && y >= 0 && y < c && dist[z][x][y] == -1 && g[z][x][y] != '#') {
dist[z][x][y] = dist[tz][tx][ty] + 1;
q.push({ z, {x, y} });
if (g[z][x][y] == 'E') return;
}
}
}
}
int main()
{
while (1) {
memset(dist, -1, sizeof dist);
scanf("%d%d%d", &l, &r, &c);
if (!l && !r && !c) break;
for (int i = 0; i < l; i++) {
for (int j = 0; j < r; j++) {
scanf("%s", g[i][j]);
}
char hang[110];
scanf("%c", hang);
}
// 找出起点和终点
int sx = 0, sy = 0, sz = 0, ex = 0, ey = 0, ez = 0;
for (int i = 0; i < l; i++) {
for (int j = 0; j < r; j++) {
for (int k = 0; k < c; k++) {
if (g[i][j][k] == 'S') sz = i, sx = j, sy = k;
if (g[i][j][k] == 'E') ez = i, ex = j, ey = k;
}
}
}
bfs(sx, sy, sz);
if (dist[ez][ex][ey] == -1) printf("Trapped!\n");
else printf("Escaped in %d minute(s).\n", dist[ez][ex][ey]);
}
return 0;
}
参考代码2(struct)
#include <cstring>
#include <iostream>
#include <algorithm>
#include <queue>
using namespace std;
const int N = 110;
// 定义三维坐标结构体
struct Pos {
int z, x, y; // 层、行、列
};
int l, r, c;
char g[N][N][N];
int dist[N][N][N];
int dx[6] = { -1, 0, 1, 0, 0, 0 }, dy[6] = { 0, 1, 0, -1, 0, 0 }, dz[6] = { 0, 0, 0, 0, 1, -1 };
void bfs(int start_x, int start_y, int start_z) {
queue<Pos> q;
dist[start_z][start_x][start_y] = 0;
q.push({ start_z, start_x, start_y }); // 使用emplace直接构造
while (!q.empty()) {
auto t = q.front(); q.pop();
// 直接访问结构体成员
for (int i = 0; i < 6; i++) {
int z = t.z + dz[i], x = t.x + dx[i], y = t.y + dy[i];
if (z >= 0 && z < l && x >= 0 && x < r && y >= 0 && y < c
&& dist[z][x][y] == -1 && g[z][x][y] != '#') {
dist[z][x][y] = dist[t.z][t.x][t.y] + 1;
q.push({ z, x, y }); // 压入新坐标
if (g[z][x][y] == 'E') return;
}
}
}
}
int main() {
while (true) {
memset(dist, -1, sizeof dist);
scanf("%d%d%d", &l, &r, &c);
if (!l && !r && !c) break;
// 读取输入
for (int i = 0; i < l; i++) {
for (int j = 0; j < r; j++) {
scanf("%s", g[i][j]);
}
getchar(); // 读取换行符
}
// 查找起点终点
Pos start = { 0, 0, 0 }, end = { 0, 0, 0 };
for (int i = 0; i < l; i++) {
for (int j = 0; j < r; j++) {
for (int k = 0; k < c; k++) {
if (g[i][j][k] == 'S') start = { i, j, k };
if (g[i][j][k] == 'E') end = { i, j, k };
}
}
}
bfs(start.x, start.y, start.z);
if (dist[end.z][end.x][end.y] == -1) {
printf("Trapped!\n");
}
else {
printf("Escaped in %d minute(s).\n", dist[end.z][end.x][end.y]);
}
}
return 0;
}
蓝桥杯:全球变暖
简单Flood fill
#include<iostream>
#include<cstring>
#include<algorithm>
#include<queue>
//#include<bits/stdc++.h>
using namespace std;
const int N = 1010;
typedef long long LL;
typedef pair<int, int> PII;
int n;
char g[N][N];
bool st[N][N];
int dx[] = { -1, 0, 1, 0 }, dy[] = { 0, 1, 0, -1 };
bool bfs(int a, int b)
{
bool res = false;
queue<PII> q;
q.push({ a, b });
st[a][b] = true;
while (!q.empty()) {
auto t = q.front(); q.pop();
int fx = 0;
for (int i = 0; i < 4; i++) {
int x = t.first + dx[i], y = t.second + dy[i];
if (x < 0 or x >= n or y < 0 or y >= n) {
fx++;
}
else if (x >= 0 && x < n && y >= 0 && y < n && g[x][y] == '#') {
fx++;
if (!st[x][y]) {
st[x][y] = true;
q.push({ x, y });
}
}
}
if (fx == 4) res = true;
}
return res;
}
int main()
{
scanf("%d", &n);
for (int i = 0; i < n; i++) {
scanf("%s", g[i]);
}
memset(st, false, sizeof st);
int cnt = 0, ans = 0;
for (int i = 0; i < n; i++) {
for (int j = 0; j < n; j++) {
if (g[i][j] == '#' && !st[i][j]) {
cnt++; // 原岛屿数量
if (bfs(i, j))
ans++; // 未被完全淹没的岛屿数量
}
}
}
printf("%d\n", cnt - ans);
return 0;
}
6.3 图论
蓝桥杯:交换瓶子
算法一:暴力枚举
1、通过观察可以发现,我们每一个数都必须回到它自己的位置上,比如 1 必须在第一位,2 必须在第二位上
2、那么我们就可以这样操作,由于每个数必须回到自己的位置,直接从 1 枚举到 n ,如果当前位置的数不等于它的下标,那么我们就必须要把它给替换掉
3、设当前位置为 i 的话,那么我们就从 i+1 开始往后枚举,直到找到对应的 a[j] 和我们的 i 相等,那么我们就把上个数交换,交换次数加一
4、容易证明这个算法的正确性,由于每个数必须回到原来的位置,所以我们这样子操作不会出现多于的步骤,因为每次操作都是必须的。
时间复杂度:$O(n^2)$
#include<iostream>
#include<cstring>
#include<algorithm>
#include<cstdio>
using namespace std;
const int N=10010;
int n;
int a[N];
int main()
{
scanf("%d",&n);
for(int i=1;i<=n;i++)scanf("%d",&a[i]);
int sum=0;
for(int i=1;i<=n;i++)
{
if(a[i]!=i)//直接遍历,如果不是自身的话,我们必然要交换,所以不会出现多于的操作
{
for(int j=i+1;j<=n;j++)
if(a[j]==i)
swap(a[i],a[j]);
sum++;
}
}
printf("%d\n",sum);
return 0;
}
算法二:置换群
时间复杂度:$O(N)$
#include<cstring>
#include<iostream>
#include<algorithm>
using namespace std;
const int N = 10010;
int n;
int b[N];
bool st[N]; // 判重,帮助找环
int main()
{
scanf("%d", &n);
for (int i = 1; i <= n; i++) scanf("%d", &b[i]);
int cnt = 0; // 记录环的数量
for (int i = 1; i <= n; i++) {
if (!st[i]) { // 如果该点没有被遍历过,说明它属于一个新的环
cnt++; // 环数+1
for (int j = i; !st[j]; j = b[j]) {
st[j] = true; // 将这个新环的所有数标记
}
}
}
printf("%d\n", n - cnt);
return 0;
}
蓝桥杯:大臣的旅费
由”同时,如果不重复经过大城市,从首都到达每个大城市的方案都是唯一的“可知,该图是无环图,且从上面可知是无环连通图,即树。
该问题属于第一种,求树的直径问题。
树形DP常见问题类型
- 树的直径:最长路径问题,每个节点维护最大和次大深度。
- 最大独立集:如“没有上司的舞会”,选择不相邻节点使总和最大。
- 最小顶点覆盖:选择最少节点覆盖所有边。
- 树的重心:删除节点后使最大连通块最小化。
- 路径和问题:如二叉树中的最大路径和。
具体做法:
- 在树中任取一个点m,找到距离m最远的点x(则点x必定是直径的端点之一)
- 找到距离点x最远的点y,则点x和点y即为该树的直径之一的两个端点。
时间复杂度:$O(N)$
#include<cstdio>
#include<cstring>
#include<iostream>
#include<algorithm>
#include<vector>
using namespace std;
const int N = 100010;
int n;
struct Edge
{
int id, w; // 邻接点和边的权值
};
vector<Edge> h[N]; // 邻接表存储图,表头h
int dist[N]; // dist[u] 表示点u到起点的距离
void dfs(int u, int father, int distance) // 当前点u,上一个点father(避免回溯),当前的距离distance
{
dist[u] = distance;
for (auto node : h[u]) {
if (node.id != father) {
dfs(node.id, u, distance + node.w); // 递归子节点,累加距离
}
}
}
int main()
{
scanf("%d", &n);
for (int i = 0; i < n - 1; i++) {
int a, b, c;
scanf("%d%d%d", &a, &b, &c);
h[a].push_back({ b, c }); // 双向添加边
h[b].push_back({ a, c });
}
dfs(1, -1, 0); // 从点1开始遍历所有点,求出每个点到起点1的距离存到dist中
int x = 1; // 找到距离起点最远的点x,x为该树的直径的端点之一
for (int i = 1; i <= n; i++) {
if (dist[i] > dist[x]) {
x = i;
}
}
dfs(x, -1, 0); // 从距离起点1最远的点x开始,求出所有点到点x的距离,并存储到dist中
int y = 1; // 找到距离点x最远的点y,则点x和点y为该树的直径之一的两个端点
for (int i = 1; i <= n; i++) {
if (dist[i] > dist[y])
y = i; // 更新y为距离x最远的点
}
int s = dist[y]; // 此时的dist[y]则为树的直径,即任意两个点的最大距离
printf("%lld\n", 10 * s + s * (s + 1ll) / 2); // 由公式计算出结果
return 0;
}
第七章 贪心
AcWing 1055. 股票买卖II
简单贪心
时间复杂度:$O(N)$
#include<iostream>
#include<cstring>
#include<algorithm>
using namespace std;
const int N = 100010;
typedef long long LL;
int n;
int prices[N];
int main()
{
scanf("%d", &n);
for (int i = 0; i < n; i++) scanf("%d", &prices[i]);
int sums = 0;
for (int i = 0; i + 1 < n; i++) {
int dp = prices[i + 1] - prices[i];
if (dp > 0) sums += dp;
}
printf("%d\n", sums);
return 0;
}
AcWing 104. 货仓选址
WA了一次,第一想法是把仓库建在最左端和最右端的商铺的正中间,通过了样例就交了……
正确做法是用仓库把所有商店一分为二,即仓库左边的商店数量和右边的商店数量要尽可能相同。
- 从最简单的情况想:如果只有一个商店,最优解肯定是仓库和商店位置相同,距离为0
- 如果有2个商店(从左到右位置分别是a和b, b > a),最优解是建在区间[a, b]中,距离之和为
b - a - 如果有3个商店(从左到右位置分别是a, b, c),则最优解是仓库和中间商店的位置相同,距离之和为
c - a - 依次类推可得出:
- 有奇数个商店时,设为n个,将所有商店按位置排序后,则应在第 $(n+1)/2$个商店上
- 有偶数个商店时,设为n个,将所有商店按位置排序后,则应在第$n/2$个商店上
数学证明
参考代码
#include<iostream>
#include<cstring>
#include<algorithm>
using namespace std;
const int N = 100010;
typedef long long LL;
int n;
int a[N];
int main()
{
scanf("%d", &n);
for (int i = 0; i < n; i++) scanf("%d", &a[i]);
sort(a, a + n);
int ans = 0, mid = a[n / 2];
for (int i = 0; i < n; i++) {
ans += abs(a[i] - mid);
}
printf("%d\n", ans);
return 0;
}
时间复杂度:$O(nlogn)$
AcWing 122. 糖果传递
参考代码
#include<iostream>
#include<cstring>
#include<algorithm>
using namespace std;
const int N = 1000010;
typedef long long LL;
int n;
int a[N];
LL c[N];
int main()
{
scanf("%d", &n);
LL sums = 0;
for (int i = 1; i <= n; i++) {
scanf("%d", &a[i]);
sums += a[i];
}
LL avg = sums / n;
for (int i = n; i > 1; i--) {
c[i] = c[i + 1] + avg - a[i];
}
c[1] = 0;
sort(c + 1, c + n + 1);
LL ans = 0;
for (int i = 1; i <= n; i++) {
ans += abs(c[i] - c[(i + 1) / 2]);
}
printf("%lld\n", ans);
return 0;
}
时间复杂度:$O(NlogN)$,可以用快速选择优化到$O(N)$
AcWing 112. 雷达设备
处理:将每个小岛可以选择的雷达坐标转换为一个区间
将原问题等价为给定n个区间,求最少选多少个点,可以使得每个区间上最少被选一个点
| 排序方式 | 时间复杂度 | 适用场景 | 优势 |
|---|---|---|---|
| 按右端点排序 | $O(n log n)$ | 最小覆盖点问题 | 保证最优解 |
| 按左端点排序 | $O(n log n)$ | 最大不相交区间数 | 不适用覆盖问题 |
这种排序方式是解决区间覆盖类问题的标准方法,正确性已被算法理论严格证明。
参考代码
#include<iostream>
#include<cstring>
#include<algorithm>
#include<unordered_map>
#include<cmath>
using namespace std;
typedef long long LL;
typedef pair<double, double> PDD;
const int N = 1010;
int n;
PDD a[N];
int main()
{
int d;
scanf("%d%d", &n, &d);
bool more = false; // 判断是否超出雷达的所有可能范围
for (int i = 0; i < n; i++) {
int x, y;
scanf("%d%d", &x, &y);
int t = abs(y);
if (t > d) more = true;
else { // 如果没有超出可能范围,则将其转换为一段可能的区间
double di = sqrt(d * d - t * t);
a[i] = {x - di, x + di};
}
}
if (more) {
printf("-1");
return 0;
}
// 按右端点降序排列
sort(a, a + n, [](PDD x, PDD y){return x.second < y.second;});
int ans = 0; // 枚举每个区间
for (int i = 0; i < n;) {
ans++; // 更新雷达数
double r = a[i].second; // 将雷达放到最右端(贪心策略),保证最优解
int j = i + 1; // 枚举后面的区间
while(j < n && a[j].first <= r) j++; // 如果区间j已经被覆盖,则跳过
i = j; // 更新需要枚举的区间
}
printf("%d", ans); // 输出答案
return 0;
}
时间复杂度:$O(NlogN)$
蓝桥杯真题:付账问题
贪心问题
为什么要排序?
如果不排序,会存在较大的 a[i] 先被处理,可能被设为较低的初始平均值,导致后续较小的 a[i] 无法调整,从而可能导致无法付清全部的账单,计算出错误的方差。
#include<iostream>
#include<cstring>
#include<algorithm>
#include<cmath>
#include<string>
using namespace std;
typedef long long LL;
const int N = 5e5 + 10;
int n;
double a[N];
double b[N];
void solve()
{
double S;
scanf("%d%lf", &n, &S);
for (int i = 1; i <= n; i++) {
scanf("%lf", &a[i]);
}
sort(a + 1, a + n + 1);
double n_avg = S / n, sums = S;
for (int i = 1; i <= n; i++) {
if (a[i] >= n_avg) {
b[i] = n_avg;
sums -= b[i];
}
else {
b[i] = a[i];
sums -= b[i];
n_avg = sums / (n - i);
}
}
// 计算方差
double ans = 0;
for (int i = 1; i <= n; i++) {
ans += (b[i] - S / n) * (b[i] - S / n);
}
ans = sqrt(ans / (n * 1.0));
printf("%.4f\n", ans);
}
int main()
{
int T = 1;
//cin >> T;
while (T--) {
solve();
}
return 0;
}
蓝桥杯真题:乘积最大
贪心+分类讨论
参考代码$O(nlogn)$
#include<iostream>
#include<cstring>
#include<algorithm>
#include<string>
using namespace std;
typedef long long LL;
const int N = 1e5 + 10, Mod = 1000000009;
int n, k;
int a[N];
void solve()
{
cin >> n >> k;
for (int i = 0; i < n; i++) cin >> a[i];
sort(a, a + n);
int res = 1;
int l = 0, r = n - 1;
int sign = 1;
if (k % 2) {
res = a[r--];
k--;
if (res < 0) sign = -1; // 说明全是负数,否则说明至少存在一个整数,取完最大的数之后变为k为偶数的情况
}
while (k) {
LL x = (LL)a[l] * a[l + 1], y = (LL)a[r - 1] * a[r];
if (x * sign > y * sign) { // 这个太妙了,全负数就选最小值
res = x % Mod * res % Mod;
l += 2;
}
else {
res = y % Mod * res % Mod;
r -= 2;
}
k -= 2;
}
cout << res << endl;
}
int main()
{
int T = 1;
//cin >> T;
while (T--) {
solve();
}
return 0;
}
蓝桥杯真题:后缀表达式
后缀表达式对应一个二叉树的后序遍历,同理,前缀表达式和中缀表达式分别对应二叉树的前序遍历和中序遍历。
特别的,中缀表达式即为最常用的“a + b”形式。
其中,前缀表达式又称波兰式,后缀表达式又称逆波兰式。
分析这题可知,如果正负号的个数都大于0,通过调整正负号的位置,可以改变正负号的个数。
更具体的说,
- 当
M=0时,则全部为加号, - 当
M>0时,则负号的个数可调整为1~M+N个
根据贪心策略,尽量减掉负数,加上正数。如果不够,那么尽量减掉较小的正数,加上较大的负数。
参考代码 $O(nlogn)$
#include<iostream>
#include<cstring>
#include<algorithm>
#include<string>
using namespace std;
typedef long long LL;
const int N = 2e5 + 10, Mod = 1000000009;
int n, m;
int a[N];
void solve()
{
cin >> n >> m;
int cnt = 0;
for (int i = 0; i < n + m + 1; i++) {
cin >> a[i];
if (a[i] < 0) cnt++;
}
sort(a, a + n + m + 1); // 也可以不排序,找出最大值最小值即可,不排序是O(n)
LL ans = 0;
if (m == 0) {
for (int i = 0; i < n + m + 1; i++) ans += a[i];
}
else {
if (cnt == 0) { // 全正,且负号的个数为 1 ~ n+m
ans -= a[0];
for (int i = 1; i < n + m + 1; i++) ans += a[i];
}
else if (cnt == n + m + 1) { // 全负,且负号的个数为 1 ~ n+m
ans = a[n + m] - a[n + m - 1];
for (int i = 0; i < n + m - 1; i++) ans += abs(a[i]);
}
else { // 至少有一个正数
for (int i = 0; i < n + m + 1; i++) ans += abs(a[i]);
}
}
cout << ans << endl;
}
int main()
{
int T = 1;
//cin >> T;
while (T--) {
solve();
}
return 0;
}
蓝桥杯真题:灵能传输
第八章 数论
AcWing 1246. 等差数列
简单题,考察最大公约数。
根据公式 $a_n = a_1 + (n-1)*d$,
可以得出 $n = (a_n - a_1) / d + 1$,即公差d越大,项数n越小。
当$a_n$最小,且$a_1$最大,且$d$最大时,项数n最小。
先将数列a排序,然后求出相邻两项的差的公共最大公约数,这个公共最大公约数就是这个数列的最大的公差d。
注意特判d == 0的情况!!
参考代码 $O(nlogn)$
#include<iostream>
#include<cstring>
#include<algorithm>
#include<string>
using namespace std;
typedef long long LL;
const int N = 1e5 + 10, Mod = 1000000009;
int n;
int a[N];
// 辗转相除法求最大公约数
int gcd(int a, int b) {
return b ? gcd(b, a % b) : a;
}
void solve()
{
cin >> n;
for (int i = 0; i < n; i++) {
cin >> a[i];
}
sort(a, a + n);
int d = a[0] - a[1];
for (int i = 1; i < n - 1; i++) {
int t = a[i + 1] - a[i];
d = gcd(d, t); // 求出相邻两项的差的公共最大公约数
if(d == 1) break;
}
int ans = 0;
if (d == 0) ans = n; // 注意特判d为0
else {
ans = (a[n - 1] - a[0]) / d + 1; // 公式推得
}
cout << ans << endl;
}
int main()
{
int T = 1;
//cin >> T;
while (T--) {
solve();
}
return 0;
}
AcWing 1295. X的因子链
线性筛+分解质因数+组合计数
参考代码$O(n+m)$
#include<iostream>
#include<algorithm>
#include<cstring>
using namespace std;
typedef long long LL;
const int N = (1 << 20) + 10;
// 线性筛质数
int primes[N], cnt; // 存所有质数
int st[N]; // 当前数有没有被筛过(是否是某个数的倍数)
int minp[N]; // 存primes[i]的最小质因子
void get_primes(int n)
{
for (int i = 2; i <= n; i++) {
if (!st[i]) minp[i] = i, primes[cnt++] = i;
for (int j = 0; primes[j] * i <= n; j++) {
st[primes[j] * i] = true; // 以质数primes[j]为倍数的所有数都筛掉
minp[primes[j] * i] = primes[j];
if (i % primes[j] == 0) break; // 保证每个合数都是被其最小质因子筛掉
}
}
}
int main()
{
get_primes(N - 1);
int fact[30], sum[N]; // 分别记录x的质因数和每个质因数的指数
int x;
while(cin >> x) {
int k = 0, tot = 0; // x的因子个数,总的指数的和
while(x > 1) {
int p = minp[x]; // 取出x的最小质因子
fact[k] = p, sum[k] = 0; // 分解质因数
while(x % p == 0) {
x /= p;
sum[k]++;
tot++;
}
k++;
}
LL res = 1;
for (int i = 1; i <= tot; i++) res *= i; // 计算tot的阶乘
for (int i = 0; i < k; i++) {
for (int j = 1; j <= sum[i]; j++) {
res /= j; // 除以每个质因子的指数的阶乘
}
}
cout << tot << ' ' << res << endl;
}
return 0;
}
AcWing 1296. 聪明的燕姿
约数之和,见算法基础课数论部分。
参考代码(太难了看不懂!)
#include<iostream>
#include<cstring>
#include<algorithm>
#include<cmath>
#include<string>
using namespace std;
typedef long long LL;
const int N = 50000;
// 线性筛质数
int primes[N], cnt; // 存所有质数
int st[N]; // 当前数有没有被筛过(是否是某个数的倍数)
int ans[N], len; // 记录答案和答案个数
void get_primes(int n)
{
for (int i = 2; i <= n; i++) {
if (!st[i]) primes[cnt++] = i;
for (int j = 0; primes[j] * i <= n; j++) {
st[primes[j] * i] = true; // 以质数primes[j]为倍数的所有数都筛掉
if (i % primes[j] == 0) break; // 保证每个合数都是被其最小质因子筛掉
}
}
}
bool is_prime(int x) // 判断质数
{
if (x < N) return !st[x];
for (int i = 0; primes[i] <= x / primes[i]; i++) {
if (x % primes[i] == 0) return false;
}
return true;
}
void dfs(int last, int prod, int s) //(上一个枚举到的质数下标,当前的数,当前剩余的数)
{
if (s == 1) { // 终止条件:剩余s为1,记录答案
ans[len++] = prod;
return;
}
// 处理s-1为质数的特殊情况
if (s - 1 > (last < 0 ? 1 : primes[last]) && is_prime(s - 1))
ans[len++] = prod * (s - 1);
// 枚举质数,尝试分解s
for (int i = last + 1; primes[i] <= s / primes[i]; i++) {
int p = primes[i];
for (int j = 1 + p, t = p; j <= s; t *= p, j += t) {
if (s % j == 0) dfs(i, prod * t, s / j); // 递归分解剩余部分
}
}
}
void solve()
{
get_primes(N - 1);
int s;
while (cin >> s) {
len = 0;
dfs(-1, 1, s);
cout << len << endl;
if (len) {
sort(ans, ans + len);
for (int i = 0; i < len; i++) cout << ans[i] << ' ';
cout << endl;
}
}
}
int main()
{
int T = 1;
//cin >> T;
while (T--) {
solve();
}
return 0;
}
AcWing 1299. 五指山
扩展欧几里得算法(裴蜀定理)
#include<iostream>
#include<algorithm>
#include<cstring>
using namespace std;
int exgcd(int a, int b, int &x, int &y)
{
if (!b) {
x = 1, y = 0; // a*1+0*0=a=d
return a;
}
int d = exgcd(b, a % b, y, x);
y -= a / b * x;
return d;
}
int main()
{
int a, b, x, y;
cin >> a >> b;
int d = exgcd(a, b, x, y);
printf("%d * %d + %d * %d = %d\n", a, x, b, y, d);
return 0;
}
本题,有 $x + bd \equiv y \pmod{n}$
即 $x + bd \equiv y + an$
$ -an + bd \equiv y - x$
其中的n、d、y、x均为给定值,根据裴蜀定理,$ax + by = gcd(a, b)$,故当且仅当$gcd(n, d)|(y - x)$时有解,否则无解(输出Impossible)。
如果有解,设解为$a', b'$,
则有$a' * n + b' * d = gcd(n, d)$(裴蜀定理),
且$y - x = k * gcd(n, d)$(否则无解),
故特解$b_0 = k * b’$,
即$b_0 = \frac{y-x}{gcd(n, d)} * b'$
找到特解$b_0$后,需找到$b$的最小值。
由于在线性同余方程中,通解$b$与特解$b_0$有如下关系:
$\boldsymbol{b = b_0 + t * \frac{n}{gcd(n, d)}, t \in \mathbb{Z}}$,(t属于整数集Z)
记$n' = \frac{n}{gcd(n, d)}$,要想求$b$的最小非负整数解$b_m$,
则可视为$b_0 = bm + k * n'$,
则最小非负整数解$\boldsymbol{b_m = {(b_0 % n') + n'} % n'}$
参考代码$O(Tlog(min(n,d)))$
#include<iostream>
#include<cstring>
#include<algorithm>
#include<cmath>
#include<string>
using namespace std;
typedef long long LL;
const int N = 50000;
// 扩展欧几里得定理
LL exgcd(LL a, LL b, LL &x, LL &y)
{
if (b == 0) {
x = 1, y = 0;
return a;
}
LL d = exgcd(b, a % b, y, x);
y -= a / b * x;
return d;
}
void solve()
{
LL n, d, x, y, a, b;
cin >> n >> d >> x >> y;
int gcd = exgcd(n, d, a, b); // 求出gcd(n, d),以及裴蜀定理的a, b,使得an + bd = gcd(n, d)
if ((y - x) % gcd) cout << "Impossible" << endl; // 如果(y - x) | gcd(n, d)
else {
b *= (y - x) / gcd; // 由b'扩大(y-x)/gcd(n,d)倍得到特解b0
n /= gcd; // n' = n / gcd(n, d)
cout << (b % n + n) % n << endl; // 求出b的最小非负整数解
}
}
int main()
{
int T = 1;
cin >> T;
while (T--) {
solve();
}
return 0;
}
蓝桥杯真题:最大比例
数论+辗转相除法+更相减损术
参考代码(好难,看不懂)
#include<iostream>
#include<cstring>
#include<algorithm>
#include<cmath>
#include<string>
using namespace std;
typedef long long LL;
const int N = 110;
int n;
LL x[N], a[N], b[N];
LL gcd(LL a, LL b)
{
return b == 0 ? a : gcd(b, a % b);
}
// 更相减损术
LL gcd_sub(LL a, LL b)
{
if (a < b) swap(a, b);
if (b == 1) return a;
return gcd_sub(b, a / b);
}
void solve()
{
cin >> n;
for (int i = 0; i < n; i++) {
cin >> x[i];
}
sort(x, x + n);
int cnt = 0;
for (int i = 1; i < n; i++) {
if (x[i] != x[i - 1]) {
LL d = gcd(x[i], x[0]);
a[cnt] = x[i] / d;
b[cnt++] = x[0] / d;
}
}
LL up = a[0], down = b[0];
for (int i = 1; i < cnt; i++) {
up = gcd_sub(up, a[i]);
down = gcd_sub(down, b[i]);
}
cout << up << '/' << down << endl;
}
int main()
{
int T = 1;
//cin >> T;
while (T--) {
solve();
}
return 0;
}
AcWing 1301. C循环
跟五指山这道题一模一样
因为是k位存储系统,因此每个数都相当于是模上$2^{k}$的结果,因此,该题是求线性同余方程
$A + xC \equiv B \pmod{2^{k}}$
求出$x$的最小非负整数解。
参考代码
#include<iostream>
#include<cstring>
#include<algorithm>
#include<cmath>
#include<string>
using namespace std;
typedef long long LL;
const int N = 110;
int n, m;
LL exgcd(LL a, LL b, LL& x, LL& y)
{
if (b == 0) {
x = 1, y = 0;
return a;
}
LL g = exgcd(b, a % b, y, x);
y -= a / b * x;
return g;
}
void solve()
{
LL a, b, c, k, x, y;
while (1) {
cin >> a >> b >> c >> k;
if (!a && !b && !c && !k) break;
LL kk = 1LL << k;
LL gcd = exgcd(c, kk, x, y);
if ((b - a) % gcd) cout << "FOREVER" << endl;
else {
x *= (b - a) / gcd;
kk /= gcd;
cout << ((x % kk) + kk) % kk << endl;
}
}
}
int main()
{
int T = 1;
//cin >> T;
while (T--) {
solve();
}
return 0;
}
蓝桥杯真题:正则问题
递归、DFS
参考代码
#include<iostream>
#include<algorithm>
#include<cstring>
using namespace std;
int k;
string str;
int dfs()
{
int res = 0;
while(k < str.size()) {
if (str[k] == '(') { // 处理 (......)
k++; // 跳过 '('
res += dfs();
k++; // 跳过 ')'
}
else if (str[k] == '|') {
k++; // 跳过 '|'
res = max(res, dfs());
}
else if (str[k] == ')') break;
else {
k++; // 跳过 'x'
res++; // 统计 'x'的数量
}
}
return res;
}
int main()
{
cin >> str;
cout << dfs() << endl;
return 0;
}
※蓝桥杯真题:糖果
经典DFS——重复覆盖问题,即用最少的行覆盖所有列。与之对应的另一个经典DFS问题是精准覆盖问题。
经典优化:
- 迭代加深 (位运算)
- 优先枚举选择最少得列
- 可行性剪枝
核心思路
- 问题建模
将每行视为一个集合,元素为列。目标是用最少的行覆盖所有列。 - 状态压缩
用二进制位表示列覆盖状态(例如,1 << c表示第c列被覆盖)。 - IDA*算法
结合迭代加深(Iterative Deepening)和启发式剪枝(A*)快速找到最优解。
A*和IDA*的区别
- A* 像专业导航:
同时分析所有可能路线,选择最优路径,但需大内存“记录所有地图”。 - IDA* 像渐进式探索:
先尝试“5分钟能到”的路线,失败后尝试“10分钟能到”的路线,只记当前路径。
参考代码(好难!!看不懂!!!)
#include<iostream>
#include<algorithm>
#include<cstring>
#include<vector>
using namespace std;
const int N = 110, M = 1 << 20;
int n, m, k;
vector<int> col[N];
int lg2[M];
int lowbit(int x) // 返回 x 的二进制表示中最低位的 1 对应的值
{
return x & -x;
}
int h(int state) // 估算从当前状态 state 覆盖所有列所需的最少行数
{
int res = 0;
// 计算未覆盖的列:i = (1 << m) - 1 - state,
for (int i = (1 << m) - 1 - state; i; i -= lowbit(i)) {
int c = lg2[lowbit(i)]; // 遍历未覆盖的列(通过 lowbit 逐个取出)
res++;
for (auto row : col[c]) i &= ~row;
}
return res;
}
bool dfs(int depth, int state)
{
// 剩余深度为 0,或启发式函数 h(state) 超过剩余深度
if (depth == 0 || h(state) > depth) return state == (1 << m) - 1;
//找到选择性最少的一列
int t = -1; // 用m个1减去当前的状态剩下的则为未选择的1
for (int i = (1 << m) - 1 - state; i; i -= lowbit(i)) {
int c = lg2[lowbit(i)];
if (t == -1 || col[t].size() > col[c].size()) {
t = c;
}
}
// 枚举选哪一行
for (auto row : col[t])
if (dfs(depth - 1, state | row))
return true;
return false;
}
int main()
{
cin >> n >> m >> k;
for (int i = 0; i < m; i++) lg2[1 << i] = i; // 预处理 lg2 数组,加速二进制位的索引查询
for (int i = 0; i < n; i++) {
int state = 0;
for (int j = 0; j < k; j++) {
int c;
cin >> c;
state |= 1 << c - 1; // 将每行转换为二进制状态
}
for (int j = 0; j < m; j++) {
if (state >> j & 1) { // 记录每列被哪些行覆盖
col[j].push_back(state);
}
}
}
for (int i = 0; i < m; i++)
{
// 对每列的行去重并排序,减少无效搜索
sort(col[i].begin(), col[i].end());
col[i].erase(unique(col[i].begin(), col[i].end()), col[i].end());
}
int depth = 0; // 递归层数
while (depth <= m && !dfs(depth, 0)) depth++; // 从 depth = 0 开始逐步增加深度限制,调用 dfs 寻找最小解
if (depth > m) depth = -1;
cout << depth << endl;
return 0;
}
第九章 复杂DP
AcWing 1050. 鸣人的影分身
经典的DP问题——整数划分问题
参考代码$O(n^{2})$
#include<iostream>
#include<algorithm>
#include<cstring>
using namespace std;
const int N = 1010;
int n, m;
int f[N][N]; // f[i][j]表示所有总和是i且被分成j个数的和的方案数
void solve()
{
memset(f, 0, sizeof f);
cin >> m >> n;
for (int i = 0; i <= n; i++) f[0][i] = 1; // 处理初始值
for (int i = 1; i <= m; i++) { // 枚举和
for (int j = 1; j <= n; j++) { // 枚举数字个数
f[i][j] = f[i][j - 1]; // 状态转移
if (i >= j) f[i][j] = f[i][j - 1] + f[i - j][j];
}
}
cout << f[m][n] << endl;
}
int main()
{
int t;
cin >> t;
while(t--) {
solve();
}
return 0;
}
AcWing 1047. 糖果
动态规划的一般状态表示为二维的f[i][j],其中第一维i表示数量,第二维j表示约束条件。而f[i][j]的值一般表示所有选法的最大值、最小值或者数量。
#include<iostream>
#include<cstring>
#include<algorithm>
using namespace std;
const int N = 110;
int n, k;
int f[N][N];
int main()
{
cin >> n >> k;
memset(f, -0x3f, sizeof f); // 初始化为负的0x3f3f3f,约为-1e9
f[0][0] = 0; // 初始状态
for (int i = 1; i <= n; i++) {
int w;
cin >> w;
for (int j = 0; j < k; j++) { // 状态转移
f[i][j] = max(f[i - 1][j], f[i - 1][(j + k - w % k) % k] + w);
}
}
cout << f[n][0] << endl;
return 0;
}
蓝桥真题:密码脱落
注意,由于状态转移方程中会用到l+1和r-1的状态,所以不能从大到小枚举或者从小到大枚举!
正确的做法是:
for 区间长度
for 左端点
右端点 = 左端点+区间长度-1
参考代码
#include<iostream>
#include<cstring>
#include<algorithm>
using namespace std;
const int N = 1010;
string s;
int f[N][N];
int main()
{
cin >> s;
int n = s.size();
for (int len = 1; len <= n; len++) {
for (int l = 0; l + len - 1 < n; l++) {
int r = l + len - 1;
if (len == 1) f[l][r] = 1; // 初始化
else {
if (s[l] == s[r]) f[l][r] = f[l + 1][r - 1] + 2;
f[l][r] = max(f[l][r], f[l][r - 1]);
f[l][r] = max(f[l][r], f[l + 1][r]);
}
}
}
cout << n - f[0][n - 1] << endl;
return 0;
}
蓝桥真题:生命之树
求一个连通块,使得这个连通块的所有(和谐)值的和最大。
参考代码
第十章 综合复习
补充
1. 简单博弈论
必胜态:存在一种操作方式,使得操作之后让对手变成必败态
必败态:不论怎么操作,对手都会变成必胜态
AcWing 5538. 回文游戏
首先,模拟可知
0 是必败态
1~9是必胜态
10是必胜态
猜想:
n如果末位是0,则必败;n如果末位不是0,则必胜
证明:
假设先手第一次取的数量是x。
记:
-
当前$n$的末位不是$0$的状态为
s1 -
当前$n$的末位是$0$的状态为
s2
分类讨论:
-
如果初始时$n$的末位不是$0$
此时状态为
s1,让$x$等于$n$的个位数字。记剩余数量$n - x = k$,则$k$的末位为$0$,即状态变为s2,对手拿的数量$x$末位必定不是0(因为回文数的末位不可能是0),则对手拿完后必定会将状态重新变为s1,我们只需要重复前面操作直到剩余$k$为0,对手输掉比赛。 -
如果初始时$n$的末位是$0$
同上,此时状态为
s2,先手只能将状态变为s1,无法将剩余数量$k$变为0,故此时先手必败。
参考代码
#include<iostream>
#include<string>
using namespace std;
string s;
int main()
{
int t;
cin >> t;
while(t--) {
cin >> s;
if (s.back() == '0') printf("E\n");
else printf("B\n");
}
return 0;
}
2. 二分查找
AcWing 5540. 最大限度地提高生产力
排序 + 二分查找
#include<iostream>
#include<queue>
#include<cstring>
#include<algorithm>
using namespace std;
typedef long long LL;
typedef pair<int, int> PII;
const int N = 200010; // 破环为链
int n, m;
int c[N], t[N];
int main()
{
int q;
scanf("%d%d", &n, &q);
for (int i = 0; i < n; i++) scanf("%d", &c[i]);
for (int i = 0; i < n; i++) scanf("%d", &t[i]), c[i] -= t[i];
sort(c, c + n);
while (q--) {
int v, s;
scanf("%d%d", &v, &s);
int l = 0, r = n;
while (l < r) { // 找到第一个能进入的农场
int mid = l + r >> 1;
if (c[mid] > s) r = mid;
else l = mid + 1;
}
if (n - r >= v) puts("YES");
else puts("NO");
}
return 0;
}
LaTex常用语法
在 LaTeX 中表示异或符号(⊕)及其转义方式如下:
1. 直接表示异或符号
-
标准符号:使用
\oplus生成 ⊕
示例:线性同余方程$ax\equiv b \pmod{n}$ -
自定义样式(如加粗、颜色等):
$\boldsymbol{\oplus}$ % 加粗 $\textcolor{red}{\oplus}$ % 红色
2. 常见逻辑/位运算符号
| 符号 | LaTeX 命令 | 示例输出 |
|---|---|---|
| 与(AND) | \land 或 \& |
$a \land b$ |
| 或(OR) | \lor 或 ` |
` |
| 非(NOT) | \lnot |
$\lnot a$ |
| 异或(XOR) | \oplus |
$a \oplus b$ |
| 同或(XNOR) | \odot |
$a \odot b$ |
3. 转义符处理
LaTeX 中需转义的字符如下表,使用反斜杠 \ 进行转义:
| 字符 | 转义写法 | 用途说明 |
|---|---|---|
% |
\% |
百分比符号 |
$ |
\$ |
数学模式开关 |
& |
\& |
表格对齐符 |
# |
\# |
宏参数编号 |
_ |
\_ |
下标 |
{ |
\{ |
分组开始 |
} |
\} |
分组结束 |
~ |
\textasciitilde 或 \~ |
非换行空格 |
^ |
\textasciicircum 或 \^{} |
上标 |
\ |
\textbackslash |
反斜杠本身 |
示例:
\$100 \& 50\%\_profit → $100 \& 50\%\_profit
4. 完整示例代码
\documentclass{article}
\usepackage{amsmath} % 数学公式支持
\usepackage{xcolor} % 颜色支持
\begin{document}
异或运算:$a \oplus b = c$
转义字符示例:
\$10 \& 20\%\_total → $10 \& 20\%\_total
自定义样式:
$\textcolor{blue}{\oplus}$ → \textcolor{blue}{$ \oplus $}
\end{document}
注意事项
- 数学模式:逻辑符号需在
$...$或$$...$$中使用。 - 包依赖:
amsmath提供标准符号,xcolor用于颜色设置。 - 兼容性:
\oplus和\odot在所有 LaTeX 发行版中默认支持。
以下是一些常见数学符号及其对应的 LaTeX 语法,按类别整理:
1. 希腊字母
| 符号 | LaTeX 语法 | 符号 | LaTeX 语法 |
|---|---|---|---|
| $\alpha$ | \alpha |
$\Alpha$ | \Alpha (不常用) |
| $\beta$ | \beta |
$\Beta$ | \Beta (不常用) |
| $\gamma$ | \gamma |
$\Gamma$ | \Gamma |
| $\delta$ | \delta |
$\Delta$ | \Delta |
| $\epsilon$ | \epsilon |
$\varepsilon$ | \varepsilon |
| $\theta$ | \theta |
$\Theta$ | \Theta |
| $\pi$ | \pi |
$\Pi$ | \Pi |
| $\mu$ | \mu |
$\Omega$ | \Omega |
2. 运算符
| 符号 | LaTeX 语法 | 说明 |
|---|---|---|
| $+$ | + |
加号 |
| $-$ | - |
减号 |
| $\times$ | \times |
乘号 |
| $\div$ | \div |
除号 |
| $\pm$ | \pm |
加减号 |
| $\mp$ | \mp |
减加号 |
| $\cdot$ | \cdot |
点乘(如 a \cdot b) |
| $\sum$ | \sum |
求和符号 |
| $\prod$ | \prod |
求积符号 |
| $\int$ | \int |
积分符号 |
| $\lim$ | \lim |
极限符号 |
| $\sqrt{x}$ | \sqrt{x} |
平方根 |
| $\sqrt[n]{x}$ | \sqrt[n]{x} |
n 次根 |
3. 关系符号
| 符号 | LaTeX 语法 | 说明 |
|---|---|---|
| $=$ | = |
等于 |
| $\neq$ | \neq |
不等于 |
| $\approx$ | \approx |
约等于 |
| $\equiv$ | \equiv |
恒等于 (同余) |
| $\leq$ | \leq |
小于等于 |
| $\geq$ | \geq |
大于等于 |
| $\subset$ | \subset |
子集 |
| $\subseteq$ | \subseteq |
包含于 |
| $\in$ | \in |
属于 |
| $\notin$ | \notin |
不属于 |
4. 箭头符号
| 符号 | LaTeX 语法 | 说明 |
|---|---|---|
| $\to$ | \to |
右箭头 |
| $\rightarrow$ | \rightarrow |
右箭头 |
| $\Rightarrow$ | \Rightarrow |
逻辑蕴含 |
| $\leftrightarrow$ | \leftrightarrow |
双向箭头 |
| $\uparrow$ | \uparrow |
上箭头 |
5. 括号与分隔符
| 符号 | LaTeX 语法 | 说明 |
|---|---|---|
| $($ | ( |
小括号 |
| $[$ | [ 或 \lbrack |
中括号 |
| ${$ | \{ 或 \lbrace |
大括号 |
| $\langle$ | \langle |
左尖括号 |
| $\lfloor$ | \lfloor |
左地板括号 |
| 自适应括号 $\left( ... \right)$ | \left( ... \right) |
根据内容调整大小 |
6. 上下标与分式
| 符号 | LaTeX 语法 | 示例 |
|---|---|---|
| 上标 | x^2 |
$x^2$ |
| 下标 | x_1 |
$x_1$ |
| 分式 | \frac{a}{b} |
$\frac{a}{b}$ |
| 连分式 | \cfrac{a}{b} |
更紧凑的分式 $\cfrac{a}{b}$ |
7. 集合与逻辑符号
| 符号 | LaTeX 语法 | 说明 |
|---|---|---|
| $\forall$ | \forall |
对所有 |
| $\exists$ | \exists |
存在 |
| $\emptyset$ | \emptyset |
空集 |
| $\mathbb{N}$ | \mathbb{N} |
自然数集 |
| $\cap$ | \cap |
交集 |
| $\cup$ | \cup |
并集 |
8. 矩阵与方程组
| 符号 | LaTeX 语法 | 示例 |
|---|---|---|
| 矩阵 | \begin{matrix} a & b \\ c & d \end{matrix} |
$\begin{matrix} a & b \ c & d \end{matrix}$ |
| 带括号矩阵 | \begin{pmatrix} a & b \\ c & d \end{pmatrix} |
$\begin{pmatrix} a & b \ c & d \end{pmatrix}$ |
| 方程组 | \begin{cases} x + y = 1 \\ x - y = 0 \end{cases} |
$\begin{cases} x + y = 1 \ x - y = 0 \end{cases}$ |
9. 微积分符号
| 符号 | LaTeX 语法 | 说明 |
|---|---|---|
| $\frac{dy}{dx}$ | \frac{dy}{dx} |
导数 |
| $\partial$ | \partial |
偏导数符号 |
| $\int_{a}^{b}$ | \int_{a}^{b} |
定积分 |
| $\oint$ | \oint |
环路积分 |
10. 其他常用符号
| 符号 | LaTeX 语法 | 说明 |
|---|---|---|
| $\infty$ | \infty |
无穷大 |
| $\nabla$ | \nabla |
梯度算子 |
| $\angle$ | \angle |
角度 |
| $\therefore$ | \therefore |
因此,所以 |
| $\because$ | \because |
因为 |
示例代码
\documentclass{article}
\usepackage{amsmath} % 支持高级数学公式
\begin{document}
分式:$\frac{a}{b}$,根号:$\sqrt{x}$,求和:$\sum_{i=1}^{n} i^2$。
矩阵:
$$
\begin{pmatrix}
1 & 2 \\
3 & 4
\end{pmatrix}
$$
积分:$\int_{0}^{\infty} e^{-x} \, dx$
\end{document}
注意事项
- 数学模式:符号需在数学模式内使用(
$...$或$$...$$)。 - 包依赖:部分符号(如矩阵)需要加载
amsmath包。 - 空格处理:LaTeX 默认忽略数学模式中的空格,需用
\,或\quad手动调整间距。
浙公网安备 33010602011771号