K04 前缀和、差分、递推 + 栈、队列、链表(第二轮系统课 · 第 4 次)

本次两个模块:

模块 内容 复用的旧讲义 本讲义新写
M7 前缀和、差分、递推 一维/二维前缀和与差分、递推式 L04 第 1–5 节 二维前缀和、二维差分、递推
M8 线性结构 栈、队列、链表 S4(vector、stack、单种括号匹配)、L07(queue) 多种括号、后缀表达式、数组模拟链表、list

一、M7 前缀和、差分、递推

盲写(15 分钟,合上讲义)

输入 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 节。

1.1 复用

内容 在哪
一维前缀和、区间求和 L04 第 1、3 节
一维差分、区间批量加 L04 第 2 节
铺设道路(差分的思想)、最大子段和 L04 第 4、5 节

1.2 二维前缀和

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。

1.3 二维差分

把矩形 (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。

1.4 递推

递推 = 写出"第 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 的高精度加法接上去。


二、M8 栈、队列、链表

盲写(15 分钟,合上讲义)

输入一行只含 ()[]{} 的字符串(长度 ≤ 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;
}

2.1 复用

内容 在哪
stack 五个操作、单种括号匹配、"取 top 前先判空" S4 1.2 节、第二节
queue 五个操作、front() 只看不删、pop() 只删不返回 L07

三种结构一句话区分:

结构 规则 什么题用
栈 后进先出 括号匹配、表达式求值、"最近的一个"
队列 先进先出 BFS、排队模拟、约瑟夫问题
链表 可以在中间 O(1) 插入和删除 "把某人插到某人左边""删掉某人"反复做很多次

2.2 后缀表达式求值

后缀表达式:运算符写在两个数后面,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 表达式求值只有 + 和 *:可以把表达式看成"若干个乘积加起来",遇到 * 就先乘,遇到 + 就把当前乘积存进总和。

2.3 链表:数组模拟

题目要"反复在中间插入、删除"时,数组要整体搬家(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 海底高铁(黄,差分)

五、30 秒自测

  1. 二维前缀和求矩形和的公式?为什么是"两个减一个加"?
  2. 二维差分给矩形加 c,要改哪四个格子?
  3. 走楼梯递推式和边界?n 到 5000 怎么办?
  4. 后缀表达式求值,弹出两个数时哪个是左操作数?
  5. 数组模拟链表,插入时为什么最后才改 R[k]?
答案
  1. s[x2][y2] - s[x1-1][y2] - s[x2][y1-1] + s[x1-1][y1-1];左上角那块被减了两次,要加回一次。
  2. (x1,y1)+c、(x1,y2+1)-c、(x2+1,y1)-c、(x2+1,y2+1)+c。
  3. f(n) = f(n−1) + f(n−2),f(1) = 1,f(2) = 2;用高精度加法。
  4. 后弹出的是左操作数 a,先弹出的是右操作数 b。
  5. 先改了 R[k],原来 k 右边的那个人就找不到了。

本次打卡:M7 ①、M8 ①(两道盲写);作业 AC 后打 ②③。