本次两个模块:
| 模块 | 内容 | 复用的旧讲义 | 本讲义新写 |
|---|---|---|---|
| M7 前缀和、差分、递推 | 一维/二维前缀和与差分、递推式 | L04 第 1–5 节 |
二维前缀和、二维差分、递推 |
| M8 线性结构 | 栈、队列、链表 | S4(vector、stack、单种括号匹配)、L07(queue) |
多种括号、后缀表达式、数组模拟链表、list |
输入 n、m、q,然后 n 个数。接下来 m 行,每行
l r c,表示把第 l 到第 r 个数都加上 c。再接下来 q 行,每行l r,输出加完之后第 l 到第 r 个数的和。n、m、q ≤ 10⁵。输入:
5 2 2 1 2 3 4 5 1 3 10 2 5 1 1 5 3 3输出:
49/14
#include <bits/stdc++.h>
using namespace std;
const int N = 100005;
long long a[N], d[N], s[N];
int main() {
int n, m, q;
cin >> n >> m >> q;
for (int i = 1; i <= n; i++) cin >> a[i];
for (int i = 1; i <= n; i++) d[i] = a[i] - a[i - 1]; // 建差分数组
while (m--) {
int l, r; long long c;
cin >> l >> r >> c;
d[l] += c;
d[r + 1] -= c; // r+1 可能等于 n+1,数组要多开
}
for (int i = 1; i <= n; i++) a[i] = a[i - 1] + d[i]; // 差分还原
for (int i = 1; i <= n; i++) s[i] = s[i - 1] + a[i]; // 前缀和
while (q--) {
int l, r;
cin >> l >> r;
cout << s[r] - s[l - 1] << endl;
}
return 0;
}写对了 → 直接看 1.2
起的新内容。写不出 → 先读 L04 第 1、2
节。
| 内容 | 在哪 |
|---|---|
| 一维前缀和、区间求和 | L04 第 1、3 节 |
| 一维差分、区间批量加 | L04 第 2 节 |
| 铺设道路(差分的思想)、最大子段和 | L04 第 4、5 节 |
s[i][j] = 左上角 (1,1) 到 (i,j)
这个矩形里所有数的和。
怎么算 s[i][j]:上面一块 + 左边一块 −
重叠的左上角一块 + 自己。
s[i][j] = s[i-1][j] + s[i][j-1] - s[i-1][j-1] + a[i][j]
怎么求任意矩形 (x1,y1)–(x2,y2) 的和:大矩形 − 上面多出来的 − 左边多出来的 + 被减了两次的左上角。
sum = s[x2][y2] - s[x1-1][y2] - s[x2][y1-1] + s[x1-1][y1-1]
📝 两个公式的符号规律一样:两个减、一个加。 记不住就在纸上画 2×2 的格子,看哪块被加了几次。
#include <bits/stdc++.h>
using namespace std;
long long s[1005][1005]; // 下标从 1 开始,第 0 行第 0 列天然是 0
int main() {
int n, m;
cin >> n >> m;
for (int i = 1; i <= n; i++)
for (int j = 1; j <= m; j++) {
long long x;
cin >> x;
s[i][j] = s[i - 1][j] + s[i][j - 1] - s[i - 1][j - 1] + x;
}
int q;
cin >> q;
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;
}⚠️ 下标从 1 开始,这样
i-1、j-1 永远不会变成 −1。
把矩形 (x1,y1)–(x2,y2) 全部加上 c,只要改四个角:
d[x1][y1] += c
d[x1][y2+1] -= c
d[x2+1][y1] -= c
d[x2+1][y2+1] += c
所有操作做完后,对 d 做一遍二维前缀和,就得到每个格子的最终值。
#include <bits/stdc++.h>
using namespace std;
int d[1005][1005];
int main() {
int n, m;
cin >> n >> m; // n×n 的格子,m 次矩形加 1
while (m--) {
int x1, y1, x2, y2;
cin >> x1 >> y1 >> x2 >> y2;
d[x1][y1]++;
d[x1][y2 + 1]--;
d[x2 + 1][y1]--;
d[x2 + 1][y2 + 1]++;
}
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= n; j++) {
d[i][j] += d[i - 1][j] + d[i][j - 1] - d[i - 1][j - 1]; // 原地做前缀和
cout << d[i][j] << (j == n ? '\n' : ' ');
}
}
return 0;
}⚠️ y2+1、x2+1 可能等于
n+1,数组至少开 n+2。
递推 = 写出"第 n 项和前面几项的关系",然后从小到大一项一项算。
走楼梯:一次走 1 级或 2 级,走到第 n 级有几种走法?
#include <bits/stdc++.h>
using namespace std;
long long f[100];
int main() {
int n;
cin >> n;
// n<=90,f(90) 约 4.6e18,long long 刚好装得下
f[1] = 1;
f[2] = 2;
for (int i = 3; i <= n; i++) f[i] = f[i - 1] + f[i - 2];
cout << f[n] << endl;
return 0;
}⚠️ 边界最容易错。 递推写完,先用 n = 1、n = 2
两组数据验证(规矩 2 的"最小"那组)。有人把边界写成
f[0] = 1; f[1] = 1; 也对,但这两套边界不能混着用。
📝 写递推的三步,和 L05 的 DP
三件套一模一样:①f(n) 表示什么;②f(n)
和前面怎么关联;③最小的几项是多少。递推就是最简单的
DP。
⚠️ 能用递推就别用递归:递归版 f(n) = f(n−1) + f(n−2) 会把同一个值重复算很多遍,n = 45 左右就超时了。
📝 课堂题 P1255 数楼梯就是这个递推,但 n 到
5000——f(5000) 有一千多位,long long 只能拿 60
分(n ≤ 50 那档)。满分要把 K02 的高精度加法接上去。
输入一行只含
()[]{}的字符串(长度 ≤ 10⁵),判断括号是否完全匹配,输出YES或NO。
([]{})→YES;([)]→NO;((→NO
#include <bits/stdc++.h>
using namespace std;
int main() {
string s;
cin >> s;
stack<char> st;
bool ok = true;
for (char c : s) {
if (c == '(' || c == '[' || c == '{') {
st.push(c);
} else {
char want = (c == ')') ? '(' : (c == ']') ? '[' : '{';
if (st.empty() || st.top() != want) { ok = false; break; } // 先判空再 top
st.pop();
}
}
if (!st.empty()) ok = false; // 左括号多了
cout << (ok ? "YES" : "NO") << endl;
return 0;
}| 内容 | 在哪 |
|---|---|
stack 五个操作、单种括号匹配、"取 top 前先判空" |
S4 1.2 节、第二节 |
queue 五个操作、front()
只看不删、pop() 只删不返回 |
L07 |
三种结构一句话区分:
| 结构 | 规则 | 什么题用 |
|---|---|---|
| 栈 | 后进先出 | 括号匹配、表达式求值、"最近的一个" |
| 队列 | 先进先出 | BFS、排队模拟、约瑟夫问题 |
| 链表 | 可以在中间 O(1) 插入和删除 | "把某人插到某人左边""删掉某人"反复做很多次 |
后缀表达式:运算符写在两个数后面,3 5 2 - * 7 +
就是
3 * (5 - 2) + 7。它不需要括号,也不需要考虑优先级。
规则:从左往右扫,遇到数就入栈;遇到运算符就弹出两个数,算完把结果入栈。最后栈里剩下的就是答案。
#include <bits/stdc++.h>
using namespace std;
int main() {
// 输入:用空格隔开的后缀表达式,例如 3 5 2 - * 7 +
stack<long long> st;
string tok;
while (cin >> tok) {
if (tok == "+" || tok == "-" || tok == "*" || tok == "/") {
long long b = st.top(); st.pop(); // 先弹出的是右边那个数
long long a = st.top(); st.pop();
if (tok == "+") st.push(a + b);
if (tok == "-") st.push(a - b);
if (tok == "*") st.push(a * b);
if (tok == "/") st.push(a / b);
} else {
st.push(stoll(tok));
}
}
cout << st.top() << endl;
return 0;
}⚠️ 最容易错的一处:先弹出来的是 b(右操作数),后弹出来的是 a。 减法和除法顺序一反,答案就错,而加法、乘法测不出来——自造数据一定要带减号。
📝 课堂题 P1449 的输入格式不一样(数后面跟
.,最后是
@),要自己改读入部分。逐个字符读,遇到数字就累加
x = x * 10 + (c - '0'),遇到 . 就把 x
入栈。
中缀表达式(平常写的
3*(5-2)+7)怎么算?
通用做法要两个栈,比较复杂。CSP-J 里出现的一般有简化,比如 P1981
表达式求值只有 + 和
*:可以把表达式看成"若干个乘积加起来",遇到
* 就先乘,遇到 + 就把当前乘积存进总和。
题目要"反复在中间插入、删除"时,数组要整体搬家(O(n)),链表只改两个指针(O(1))。
用两个数组 L[x]、R[x] 记录 x
左边、右边是谁。0 号当作"哨兵":R[0]
是第一个人,L[0] 是最后一个人。
#include <bits/stdc++.h>
using namespace std;
const int N = 100005;
int L[N], R[N];
bool removed[N];
void insertRight(int k, int x) { // 把 x 插到 k 的右边
R[x] = R[k];
L[x] = k;
L[R[k]] = x;
R[k] = x;
}
void erase_(int x) { // 删掉 x
if (removed[x]) return; // 删过的不能再删
removed[x] = true;
R[L[x]] = R[x];
L[R[x]] = L[x];
}
int main() {
R[0] = 0; L[0] = 0; // 空链表:哨兵自己连自己
insertRight(0, 1); // 1
insertRight(1, 3); // 1 3
insertRight(1, 2); // 1 2 3
insertRight(0, 4); // 4 1 2 3
erase_(2); // 4 1 3
erase_(2); // 再删一次,不应出错
for (int x = R[0]; x != 0; x = R[x]) cout << x << " ";
cout << endl;
return 0;
}⚠️ 插入时四行的顺序不能乱:要先用 R[k]
设置 x 的右边,最后才改 R[k]。先改了
R[k],原来的右邻居就找不到了。在纸上画三个方框、四根箭头,一根一根改。
STL 的 list 也能做同样的事(大纲 2.1.2
列了 list):
#include <bits/stdc++.h>
using namespace std;
int main() {
list<int> lst = {1, 2, 3};
auto it = lst.begin(); // 指向 1
++it; // 指向 2
lst.insert(it, 9); // 插到 2 的前面:1 9 2 3
it = lst.erase(it); // 删掉 2,it 指向下一个(3):1 9 3
lst.push_front(0); // 0 1 9 3
for (int x : lst) cout << x << " ";
cout << endl;
return 0;
}⚠️ list 不能用下标
lst[i],只能用迭代器一个一个走;erase
之后原来的迭代器作废,要用返回值接住。考场上数组模拟更可控,list
了解即可。
| 环节 | 题目 | 要点 |
|---|---|---|
| 课堂题 1 | P3397 地毯(橙) | 1.3 节二维差分 |
| 课堂题 2 | P1255 数楼梯(橙) | 1.4 节递推 + K02 高精度加法;先交 long long
版看能拿几分 |
| 课堂题 3 | P1449 后缀表达式(橙) | 2.2 节,改读入 |
| 课堂题 4 | P1996 约瑟夫问题(橙) | 用 queue 模拟:数到的出队,没数到的出队再入队 |
| 小测 1(20 分钟) | P2004 领地选择(橙) | 二维前缀和;n 到 1000,枚举每个正方形的左上角 |
| 小测 2(20 分钟) | P1241 括号序列(橙) | 题面给了配对规则,照着规则模拟,别自己发明 |
| 档 | 必做(4 道) | 选做 |
|---|---|---|
| B 档 | ① P8218 求区间和(橙) ② P2367 语文成绩(橙,差分) ③ P1002 过河卒(橙,递推 + 障碍) ④ P1540 机器翻译(橙,队列) | P1981 表达式求值(橙) |
| A 档 | ① P1002 过河卒(橙) ② P1719 最大加权矩形(黄,二维前缀和) ③ P1981 表达式求值(橙) ④ P1160 队列安排(黄,链表) | P4387 验证栈序列(黄)、P3406 海底高铁(黄,差分) |
R[k]?s[x2][y2] - s[x1-1][y2] - s[x2][y1-1] + s[x1-1][y1-1];左上角那块被减了两次,要加回一次。(x1,y1)+c、(x1,y2+1)-c、(x2+1,y1)-c、(x2+1,y2+1)+c。R[k],原来 k 右边的那个人就找不到了。本次打卡:M7 ①、M8 ①(两道盲写);作业 AC 后打 ②③。