目录

蓝桥杯105 油漆面积

蓝桥杯109 分考场

蓝桥杯110 合根植物

蓝桥杯111 区间移位

蓝桥杯113 填字母游戏

蓝桥杯117 碱基

蓝桥杯118 机器人塔

蓝桥杯121 压缩变换

蓝桥杯123 取球博弈

蓝桥杯126 交换瓶子


蓝桥杯105 油漆面积

题目链接:https://www.lanqiao.cn/problems/105/learning/

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

const int MAX = 10001; // 坐标范围0~10000,数组开10001足够
bool covered[MAX][MAX] = {false}; // 标记方格是否被覆盖,初始全为false

int main() {
    int n;
    cin >> n;
    
    for (int k = 0; k < n; k++) {
        int x1, y1, x2, y2;
        cin >> x1 >> y1 >> x2 >> y2;
        
        // 确保x1<x2,y1<y2(处理对角点顺序问题)
        if (x1 > x2) swap(x1, x2);
        if (y1 > y2) swap(y1, y2);
        
        // 遍历矩形内的每个单位方格,标记为已覆盖(只标记一次)
        for (int x = x1; x < x2; x++) {
            for (int y = y1; y < y2; y++) {
                covered[x][y] = true;
            }
        }
    }
    
    // 统计所有被覆盖的方格数量(即总面积)
    int total = 0;
    for (int x = 0; x < MAX; x++) {
        for (int y = 0; y < MAX; y++) {
            if (covered[x][y]) {
                total++;
            }
        }
    }
    
    cout << total << endl;
    return 0;
}

蓝桥杯109 分考场

题目链接:https://www.lanqiao.cn/problems/109/learning/

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

const int N = 110;
bool know[N][N];  // 标记两人是否认识,know[a][b]=true表示a和b认识
int room[N][N];   // 考场安排:room[i][j]表示第i个考场的第j个位置坐的人编号
int min_room = N; // 最少考场数,初始设为最大可能值n
int n, m;

// dfs:安排第x个人,当前已用room_num个考场
void dfs(int x, int room_num) {
    // 剪枝:当前考场数已≥最优解,无需继续
    if (room_num >= min_room) return;
    // 所有人体安排完毕,更新最优解
    if (x > n) {
        min_room = room_num;
        return;
    }
    
    // 尝试把第x个人安排到已有的每个考场
    for (int i = 1; i <= room_num; i++) {
        int j = 1;
        // 找到考场i中第一个空位置(且该位置所有人都不认识x)
        while (room[i][j] && !know[x][room[i][j]]) j++;
        // 找到空位置,安排x入座
        if (room[i][j] == 0) {
            room[i][j] = x;
            dfs(x + 1, room_num); // 安排下一个人
            room[i][j] = 0;       // 回溯:撤销安排
        }
    }

    // 所有已有考场都坐不下,新开一个考场
    room[room_num + 1][1] = x;
    dfs(x + 1, room_num + 1);
    room[room_num + 1][1] = 0; // 回溯:撤销新考场的安排
}

int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);

    cin >> n >> m;
    // 初始化人际关系表
    for (int i = 0; i < m; i++) {
        int a, b;
        cin >> a >> b;
        know[a][b] = know[b][a] = true; // 互相认识
    }

    dfs(1, 1); // 从第1个人开始安排,初始1个考场
    cout << min_room << '\n';
    return 0;
}

蓝桥杯110 合根植物

题目链接:https://www.lanqiao.cn/problems/110/learning/

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

const int MAX = 1000010; // 最大格子数:1000*1000=1e6,开1e6+10足够
int parent[MAX];         // 记录每个格子的父节点(并查集核心)

// 查找x的根节点,带路径压缩(优化查找效率)
int find(int x) {
    return parent[x]==x ? x : parent[x]=find(parent[x]); 
}

int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);

    int m, n, k;
    cin >> m >> n >> k;
    int total = m * n; // 总格子数
    
    // 初始化并查集:每个格子的父节点是自己
    for (int i = 1; i <= total; ++i) {
        parent[i] = i;
    }
    
    // 处理k次合根操作
    for (int i = 0; i < k; ++i) {
        int a, b;
        cin >> a >> b;
        int root_a = find(a); // 找a的根
        int root_b = find(b); // 找b的根
        if (root_a != root_b) { // 根不同则合并
            parent[root_a] = root_b;
        }
    }
    
    // 统计根节点数量(即合根植物总数)
    int count = 0;
    for (int i = 1; i <= total; ++i) {
        if (parent[i] == i) { // 根节点的父节点是自己
            count++;
        }
    }
    
    cout << count << '\n';
    return 0;
}

Q:可以去掉ins.resize(n) 吗?
A:不可以。
如果删掉ins.resize(n),ins是一个空的vector<Interval>(size()=0),执行cin >> ins[i].a >> ins[i].b时,i从 0 到 n-1,而ins的有效下标范围是[0, size()-1](此时 size=0,无有效下标)
这会触发未定义行为(通常是程序崩溃、输出乱码,或运行时错误)

举个简单例子:

vector<Interval> ins; // 空vector,size=0
ins[0].a = 10; // 错误!下标0超出了空vector的范围

蓝桥杯111 区间移位

题目链接:https://www.lanqiao.cn/problems/111/learning/

每个区间 [a,b] 可以左右移动,但移动的最大距离不超过 k ,所以移动后区间的可能范围是 [a−k, b+k]。现在的问题是能不能用这些 “可伸缩的区间”,拼接出一个连续区间,覆盖 [0, 10000]?

这样就把移位问题变成了每个区间有一个可选的覆盖范围 [a−k, b+k],选一个具体位置让它们连起来覆盖目标。算法竞赛里,区间覆盖问题的最优解法就是贪心

区间覆盖问题的贪心策略:
从左往右覆盖,每次选能接上当前覆盖终点、且能覆盖到最右边的区间。

为什么要这样?因为要尽可能把覆盖范围往右推,减少后续需要的区间数量,最快达到目标 10000。
在这个题里可以拆成两个动作:
能衔接的区间:区间的可选范围 [a−k, b+k] 必须和当前已覆盖的 [0, cover] 有交集,也就是代码里的核心条件 a−k ≤ cover && b+k ≥ cover
能推最远的区间:找到衔接区间后,要让 cover 尽可能变大,这就是代码里两种 cover 更新规则的来源(本质是计算这个区间能覆盖到的最右端)


(1)初始化:
复制原始区间到 temp,因为要删除用过的区间,避免修改原数据;
cover = 0 ,从起点 0 开始覆盖;
ok = false,标记本轮是否找到可用区间。

(2)循环找区间:
遍历 temp,找第一个满足衔接条件的区间(为什么是第一个?因为原区间已经按 b 升序排序,前面的区间右端点更小,不会影响后续选择)

找到后,计算这个区间能覆盖的最右端,对应两种 cover 更新情况:
a. 区间右移就能接上 cover,覆盖最右端是 cover + (b−a)(区间本身长度)
b. 区间需要左移才能接上 cover,覆盖最右端是 b + k(区间右移到极限的位置)

删除用过的区间,避免重复使用;
标记 ok = true,进入下一轮覆盖。

(3)终止条件:
要么 cover ≥ 10000,成功覆盖,返回 true;
要么找不到衔接区间,失败,返回 false。

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

struct Interval { double a, b; };
vector<Interval> ins;
int n;

bool check(double k) {
    vector<Interval> temp = ins; 
    double cover = 0; 
    while (true) {
        bool ok = false; 
        for (int i = 0; i < temp.size(); i++) {
            double a = temp[i].a, b = temp[i].b;
            if (a - k <= cover && b + k >= cover) {
                ok = true; 
                if (a + k >= cover) {
                    cover += (b-a);
                } else {
                    cover = b + k;
                }
                temp.erase(temp.begin() + i); 
                break;  
            }
        }
        // 终止条件:要么覆盖到10000(成功),要么找不到能接的区间(失败)
        if (cover >= 10000 || !ok) break;
    }
    return cover >= 10000;
}

int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);

    cin >> n;
    ins.resize(n);
    for (int i = 0; i < n; i++) cin >> ins[i].a >> ins[i].b;
    // 按右端点排序(贪心基础,不能省)
    sort(ins.begin(), ins.end(), [](Interval x, Interval y) {
        return x.b < y.b;
    });

    // 无脑枚举:步长0.5,从小到大试
    for (double k = 0; k <= 10000; k += 0.5) {
        if (check(k)) {
          cout << k << '\n';
            return 0;
        }
    }
    return 0;
}

temp.begin() 是迭代器(可以理解为 “指向容器第一个元素的指针”),不是数字 0;
vector 的 erase 函数只接受迭代器参数,不接受整数下标,所以 temp.erase(i) 会直接编译报错;
temp.begin() + i 的作用是:从 “第一个元素的迭代器” 往后偏移 i 步,得到 “指向第 i 个元素的迭代器”,这是 vector 删除指定下标元素的标准写法。

这个题也可以不使用结构体,如下修改:

vector<Interval> ins 改成 vector<pair<int, int>> ins
访问成员:a/b 改成 first/second
排序 lambda 的参数类型改为 pair
输入赋值改为 ins[i] = {a, b};

int n;
vector<pair<int, int>> ins;  // 改成 pair
int a = temp[i].first, b = temp[i].second;  // 访问方式改为 first/second
    sort(ins.begin(), ins.end(), [](const pair<int, int>& x, const pair<int, int>& y) {
        return x.second < y.second;  // 按右端点排序
    });
    for (int i = 0; i < n; i++) {
        double a, b;
        cin >> a >> b;
        ins[i] = {a, b};  // 赋值方式
    }

蓝桥杯113 填字母游戏

 题目链接:https://www.lanqiao.cn/problems/113/learning/

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

// 记忆化缓存
unordered_map<string, int> memo;

int dfs(string s) {
    
    // 先查缓存,有结果直接返回,避免重复计算
    if (memo.find(s) != memo.end()) return memo[s];

    // 有LOL,当前玩家输
    if (s.find("LOL") != string::npos) return memo[s] = -1;
    // 没空格,平局
    if (s.find('*') == string::npos) return memo[s] = 0;

    bool can_tie = false;
    for (int i = 0; i < s.size(); ++i) {
        if (s[i] == '*') {
            // 试填L
            s[i] = 'L';
            int res = dfs(s);
            s[i] = '*';
            if (res == -1) return memo[s] = 1;  // 对手输,我赢
            if (res == 0) can_tie = true;

            // 试填O
            s[i] = 'O';
            res = dfs(s);
            s[i] = '*';
            if (res == -1) return memo[s] = 1;  // 对手输,我赢
            if (res == 0) can_tie = true;
        }
    }
    // 不能赢,判断能否平
    return memo[s] = (can_tie ? 0 : -1);
}

int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);

    int n;
    cin >> n;
    while (n--) {
        string s;
        cin >> s;
        memo.clear();
        cout << dfs(s) << '\n';
    }
    return 0;
}

Q:为什么 int res = dfs(s); 这里的res代表的是对手的结果

A:代码中 int res = dfs(s);本质是我(当前玩家)走了一步棋(把 * 改成 L/O),生成了新的字符串 s,然后把这个新棋局交给对手,调用 dfs(s) 就是计算对手在这个新棋局下,用最优策略玩的最终结果
 

Q:为什么代码里只处理了 res == -1(对手输)和 res == 0(对手平局),没有显式处理 res == 1(对手赢)的情况?

A:代码的逻辑是遍历所有可能的走法,找对自己最有利的结果

res = dfs(s) 的返回值是对手在新棋局下的最优结果
res = -1:对手必输,我必赢(最优选择,直接返回 1)
res = 0:对手只能平局,我也能平局(标记 can_tie)
res = 1:对手必赢,我走这步会输(放弃这步,试其他走法)

如果某一步的 res == 1,说明我走这步后,对手必赢,这是对我最不利的选择,所以代码不会做任何操作。既不返回,也不标记,只是继续遍历下一个可填位置,看看有没有更好的走法(比如能赢或平局的)

Q:什么是记忆化?

A:记忆化(也叫缓存)是一种优化递归或动态规划算法的核心技巧,本质是:
把递归过程中已经计算过的状态结果存起来(用哈希表或数组),下次再遇到相同状态时,直接返回已存的结果,避免重复计算。

Q:memo[s] 属于谁?

A:memo[s] 既不专属对手,也不专属自己,而是专属 “状态 s” 的固定结果。memo[s] 存储的是当棋局处于状态 s 时,当前执棋的玩家在最优策略下的最终结果。memo[s] 只和棋局状态s 绑定,和 是你走还是对手走无关,谁处于状态 s 当当前玩家,memo[s] 就是谁的结果。

蓝桥杯117 碱基

题目链接:https://www.lanqiao.cn/problems/117/learning/

第一步:枚举所有合法组合

题目要求选m个物种,最直接的办法就是用DFS/回溯枚举所有C(n,m)种组合。

第二步:对每个组合计算贡献

选好m个物种后,题目要统计“子串在所有选中物种里的出现次数乘积之和”。这里的小技巧是锚定第一个物种的子串,因为如果遍历所有可能的k长子串,范围太大,但第一个物种的k长子串数量是有限的,而且其他物种里的子串只有和它匹配才会有贡献,这样就缩小了枚举范围。

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

typedef long long ll;
const int MOD = 1e9 + 7;
int n, m, k;         // n:物种数,m:选的数量,k:子串长度
vector<string> s;    // 存储所有物种的DNA 
vector<int> chosen;  // 选中的m个物种下标
ll ans = 0;
vector<unordered_map<string, int>> sub_count;  // 预处理每个字符串的k长子串计数

// 预处理函数,统计每个字符串的k长子串出现次数
void preprocess(int idx) {
    const string& t = s[idx];
    unordered_map<string, int> cnt;
    int len = t.size();
    for (int i = 0; i + k <= len; i++) {
        string sub = t.substr(i, k);
        cnt[sub]++;
    }
    sub_count.push_back(cnt);
}

// DFS选m个物种
void dfs(int idx) {

    // 终止条件1:选够m个物种,计算该组合的贡献
    if (chosen.size() == m) {
      
        // 只处理第一个选中物种
        int first_id = chosen[0];
        
        for (auto& p : sub_count[first_id]) {  // 遍历第一个物种的所有k长子串
            string sub = p.first;  // 取出子串
            int cnt_first = p.second;  // 取出第一个物种中该子串的次数
            
            ll mul = cnt_first; // 第一个物种的次数
            // 遍历剩余选中的物种
            for (int i = 1; i < chosen.size(); i++) {
                int id = chosen[i];
                mul = mul * sub_count[id][sub] % MOD;
            }
            ans = (ans + mul) % MOD;
        }
        return;
    }
    // 终止条件2:遍历完所有物种,直接返回
    if (idx >= n) return;

    // 先试试不选第idx个物种
    dfs(idx + 1);  // 去处理下一个物种
    

    // 再试试选第idx个物种
    // 把第idx个物种加入选中列表
    chosen.push_back(idx);
    dfs(idx + 1);  // 去处理下一个物种 写成dfs(idx)会重复处理当前物种
    chosen.pop_back();   // 处理完后,把第idx个物种从选中列表删掉(恢复原样)
}

int main() {
    cin >> n >> m >> k;
    s.resize(n);  // 新增:动态初始化vector大小,适配任意n
    for (int i = 0; i < n; i++) {
        cin >> s[i];
        preprocess(i);  // 调用预处理函数
    }
    
    dfs(0);
    cout << ans % MOD << endl;
    return 0;
}

蓝桥杯118 机器人塔

 题目链接:https://www.lanqiao.cn/problems/118/learning/

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

char tower[50][50];  // 储存塔
int M, N, layer;
long long ans = 0;


void buildTower(){
  for(int i = layer; i >= 2; --i){
    for(int j = 1; j < i; ++j){
      if(tower[i][j] == tower[i][j+1])
        tower[i-1][j] = 'A';
      else
        tower[i-1][j] = 'B';
    }
  }
}

bool checkCount(){
  int cntA = 0, cntB = 0;
  for(int i = 1; i <= layer; ++i){
    for(int j = 1; j <= i; ++j){
      if(tower[i][j] == 'A') cntA++;
      else cntB++;
    }
  }
  return cntA == M && cntB == N;
}

void dfs(int pos){
  // pos 表示当前正在处理底层(第layer层)的第pos个格子
  if(pos > layer){
    buildTower();
    if (checkCount()) ans++;
    return;
  }

  // 情况1:当前位置填'A'
  tower[layer][pos] = 'A';
  dfs(pos + 1);

  // 情况2:当前位置填'B'
  tower[layer][pos] = 'B';
  dfs(pos + 1);
}


int main(){
  cin >> M >> N;
  int sum = M + N;
  layer = (sqrt(1 + 8 * sum) - 1) / 2;

  dfs(1);

  cout << ans << endl;
  return 0;
}

Q:layer = (sqrt(1 + 8 * sum) - 1) / 2;是怎么得到的?

塔的形状是金字塔形:第1层1个格子,第2层2个格子,第3层3个格子...
总共有 layer 层
总格子数 sum = M + N
金字塔的总格子数是一个等差数列的和 1 + 2 + 3 + ... + layer

等差数列求和公式 S = n × (a₁ + aₙ) ÷ 2
S = 总和(就是 sum)
n = 项数(就是 layer)
a₁ = 首项 = 1
aₙ = 末项 = layer

代入后 sum = layer × (1 + layer) ÷ 2
两边乘以2后 2 × sum = layer × (layer + 1)
展开后 layer² + layer - 2 × sum = 0
对于一元二次方程 ax² + bx + c = 0   x = [-b ± √(b² - 4ac)] ÷ (2a)
代入后 layer = [-1 ± √(1 + 8 × sum)] ÷ 2
因为层数必须是正数,所以取正号 layer = [√(1 + 8 × sum) - 1] ÷ 2

Q:为什么是i>=2以及j<i?

规则是根据第 i 层的两个相邻格子,推导第 i-1 层的一个格子
所以tower[i-1][j] 由 tower[i][j] 和 tower[i][j+1] 决定

i = layer:从最底层开始(第4层)
i >= 2:推导到第2层为止
注意数组是从[1][1]开始,而不是[0][0]开始的,这样的话第几层就有几个格子
【层数代表对应层格子数】

j = 1:从第1列开始
j < i:到第 i-1 列结束
为什么是 j < i 而不是 j <= i?
推导 tower[i-1][j] 需要 tower[i][j] 和 tower[i][j+1]

当 j = i 时:
需要 tower[i][i] 和 tower[i][i+1]
但 tower[i][i+1] 不存在(越界了)

Q:为什么后面又 j<=i

因为用途不同,前者目的是根据已知的第i层,推导第i-1层
条件是j < i ,因为需要 tower[i][j+1],不能越界,函数逻辑是自底向上

后者目的是遍历金字塔的所有格子进行统计
条件是j <= i ,因为第i层有i个格子

if(pos > layer){  // 底层填满了
    // 处理这个完整解
    return;  // 这个解处理完了,返回上一层
}

Q: 为什么不可以去掉 if (checkCount()) ?

底层:A A A
推导的塔:
   A
  A A
 A A A
统计:A=6, B=0 不符合要求,但错误代码会计数

蓝桥杯121 压缩变换

 题目链接:https://www.lanqiao.cn/problems/121/learning/

#include <iostream>
using namespace std;

const int N = 1e5 + 10;
int a[N]; // 储存原序列
int c[N]; // 标记最后一次出现

int main(){
  int n;
  cin >> n;
  for(int i = 1; i <= n; ++i){
    cin >> a[i];

    // 判断当前数字是否首次出现(暴力遍历)
    bool is_first = true;
    int last_pos = 0;
    for(int j = 1; j < i; ++j){
      if(a[j] == a[i]){
        is_first = false;
        last_pos = j; // 更新目前整数在它之前的最后一次出现的相同整数的位置
      }
    }
    if(is_first){ // 首次出现
      cout << -a[i] << " ";
      c[i] = 1;
    } else {  // 重复出现
      int cnt = 0;
      for(int j = last_pos + 1; j < i; ++j) cnt += c[j];

      cout << cnt << " ";
      c[last_pos] = 0; // 因为目前整数为新的最后一次 故旧最后一次置0
      c[i] = 1; // 新最后一次置1
    } 
  }
  return 0;
}

c[i]=1表示位置i是某数字的最后一次出现,0表示不是

蓝桥杯123 取球博弈

 题目链接:https://www.lanqiao.cn/problems/123/learning/

#include <iostream>
using namespace std;

char dp[1005][2][2] = {0}; // 储存的是先手的结果
int take[3];

char dfs(int sum, int f, int s){
  if(dp[sum][f][s]) return dp[sum][f][s]; // 记忆化

  // for循环判断是否终止 无法取球则终止
  bool can_take = false; 
  for(int i = 0; i < 3; ++i){
    if(sum >= take[i]){
      can_take = true;
      break;
    }
  }

  // 上面的for循环没进去说明球被取完 终止 胜负判断
  if(!can_take){
    if((f%2) == (s%2)) return dp[sum][f][s] = '0'; // 奇偶相同 平局
    // 先手为奇数 赢 先手为偶数 输
    return dp[sum][f][s] = (f%2 == 1) ? '+' : '-';
  }

  char res = '-'; // 初始化结果为先手必败

  // 尝试所有可能结果
  for(int i = 0; i < 3; ++i){
    if(sum >= take[i]){
      // 先手取球 更新奇偶性
      int new_f = (f + take[i]) % 2;

      // 先手取球完成 开始后手取球 先后手互换
      char opp = dfs(sum - take[i], s, new_f); 
      
      // 对手必败 先手胜
      if(opp == '-') return dp[sum][f][s] = '+';
      // 双方平局
      if(opp == '0') res = '0'; // 不直接返回是因为后面可能有胜利走法
    }
  }
  return dp[sum][f][s] = res;

}

int main(){
  cin >> take[0] >> take[1] >> take[2];
  for(int i = 0; i < 5; ++i){
    int n;
    cin >> n;
    cout << dfs(n, 0, 0) << " ";
  }
  return 0;
}

先思考需要记录什么状态?
1. 剩余球数 sum
2. 当前先手的奇偶性
3. 当前后手的奇偶性
胜负只与奇偶有关,所以用0/1表示

char dp[1005][2][2] = {0}; // 这是最先确定的

蓝桥杯126 交换瓶子

 题目链接:https://www.lanqiao.cn/problems/126/learning/

#include <iostream>
using namespace std;

const int N = 1e4 + 5;
int a[N]; // 存储瓶子排列

int main() {
    int n, ans = 0;
    cin >> n;
    for (int i = 1; i <= n; ++i) {
        cin >> a[i];
    }
    
    for (int i = 1; i <= n; ++i) { // 题目中下标从1开始
        while (a[i] != i) {
            swap(a[i], a[a[i]]); // 把当前瓶子交换到它该在的位置
            ans++; 
        }
    }
    
    cout << ans << endl;
    return 0;
}

Logo

DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。

更多推荐