ACM笔记 学习版本

https://oi-wiki.org/ds/leftist-tree/ 是个好东西

cpp 复制代码
ios::sync_with_stdio(false);
cin.tie(0);

devc++ F9编译 F10运行。

超级头文件 bits万能头无法在部分编译器使用

cpp 复制代码
// C++ 基础与 I/O
#include <iostream>
#include <cstdio>
#include <iomanip>
#include <fstream>
#include <sstream>
#include <cstdlib>
#include <cstring>

// 容器与数据结构
#include <vector>
#include <list>
#include <deque>
#include <stack>
#include <queue>
#include <map>
#include <unordered_map>
#include <set>
#include <unordered_set>
#include <bitset>
#include <array>

// 算法与数学函数
#include <algorithm>
#include <cmath>
#include <numeric>
#include <functional>
#include <utility>
#include <iterator>

// 字符串处理
#include <string>
#include <cctype>

// 时间与随机数
#include <ctime>
#include <chrono>
#include <random>

// 智能指针与内存管理
#include <memory>

初始化

cpp 复制代码
long long num = 0, money = 0;

初始化参数列表

cpp 复制代码
Node(int x1, int y1, int x2, int y2, int l)
: x1(x1), y1(y1), x2(x2), y2(y2), lay(l) {}

我chovy 你是不是用迭代器不打括号??!给我打好了啊!

cpp 复制代码
if (px >= (*it).x1 && px <= v.x2 && py <= v.y2)

你是不是先擦除再使用了?!一定要先拷贝一份再用

cpp 复制代码
vec.insert(vec.begin(), (*it));//放到最前端
vec.erase(it);

你是傻逼吗?在用it的迭代中删了it???

不要在迭代器的for中进行删除,要用下标进行访问,记录下标,break出循环后再erase,这样可以避免逆序迭代无法删除的问题。

所有的push_backTMD是从后扔进去!

你有没有手贱把j循环的自增打成i?!

你TM调试信息是不是忘记注释了?!

你有没有把一些变量写在循环里了?!

你有没有多次输入的时候清空变量?!

你TM在贪心/DP中写break了吗?不然怎么跳出去?

输出小数的除法你加(double)A/B了吗?

for输出后你换行了吗?!

二维数组引用传参

取余不变性

前置空格法

cpp 复制代码
    for(int i = 0; i < n; i++){
        for(int j = 0; j < n; j++){
            if(j > 0) cout << " ";      // 防坑:非首元素前加空格
            cout << A[i][j];
        }
        cout << endl;
    }

struct要加分号!!!

注意二维数组坐标问题

注意二维数组题目的初始坐标问题!!!这里是正整数uv,因此在遍历图的时候是从1开始的,那么前置空格法也是要求if(j>1) cout<<" ";

https://www.luogu.com.cn/problem/T419391

cpp 复制代码
#include<iostream>
using namespace std;
int A[1005][1005],n,m;

int main(){
    cin>>n>>m;
    int n1,n2;
    for(int i=0;i<m;i++){
        cin>>n1>>n2;
        A[n1][n2]=1;
        A[n2][n1]=1;
    }

    for(int i=1;i<=n;i++){
        for(int j=1;j<=n;j++){
            
            if(j!=1) cout<<" ";
            cout<<A[i][j];
            
        }
        cout<<endl;
    }
    for(int i=1;i<=n;i++){
        int d=0;
        for(int j=1;j<=n;j++){
            if(A[i][j]) d++;
        }
        cout<<d;
        for(int j=1;j<=n;j++){
            if(A[i][j]) cout<<" "<<j;
        }
        cout<<endl;
    }
}

Algorithm API

1. 返回最值元素的迭代器

  • max_element(begin, end) / min_element(begin, end)

    • 作用 :返回容器(如数组、vector)中最大/最小元素的迭代器 (指针)。注意需要加 * 取值。

    cpp 复制代码
    vector<int> v = {3, 1, 4, 1, 5};
    auto it = max_element(v.begin(), v.end());
    cout << *it << endl;      // 输出: 5 (最大值)
    cout << it - v.begin();   // 输出: 4 (最大值的下标)

2.全排序

  • next_permutation(begin, end)

    • 作用 :将序列变为"下一个"字典序更大的排列。如果是最后一个排列(降序),则变回第一个排列(升序)并返回 false

    cpp 复制代码
    string s = "abc";
    do {
        cout << s << " "; // 输出: abc acb bac bca cab cba
    } while(next_permutation(s.begin(), s.end()));
  • prev_permutation(begin, end)

    • 作用:与上面相反,求"上一个"字典序更小的排列。

    cpp 复制代码
    string s = "cba";
    do {
        cout << s << " "; // 输出: cba cab bca bac acb abc
    } while(prev_permutation(s.begin(), s.end()));

3.原地反转元素

  • reverse(begin, end)

    • 作用:原地反转区间内的元素。

    cpp 复制代码
    vector<int> v = {1, 2, 3, 4};
    reverse(v.begin(), v.end());
    // v 变为: {4, 3, 2, 1}

4.查找目标值的迭代器

  • find(begin, end, value)

    • 作用 :线性查找第一个等于 value 的元素,返回迭代器。找不到返回 end

    cpp 复制代码
    vector<int> v = {10, 20, 30};
    auto it = find(v.begin(), v.end(), 20);
    if(it != v.end()) cout << "Found!"; // 输出: Found!

5.统计值的出现次数

  • count(begin, end, value)

    • 作用:统计某个值出现的次数。

    cpp 复制代码
    vector<int> v = {1, 2, 2, 3, 2};
    cout << count(v.begin(), v.end(), 2); // 输出: 3

6.二分查找

  • binary_search(begin, end, value)

    • 作用二分查找 。前提是序列必须有序 。返回 bool。时间复杂度 O(log⁡n)。

    cpp 复制代码
    vector<int> v = {1, 3, 5, 7, 9}; // 必须有序
    bool found = binary_search(v.begin(), v.end(), 5);
    cout << found; // 输出: 1 (true)

7.去重

  • unique(begin, end)

    • 作用去重 (仅针对相邻重复元素)。它不会删除元素,而是把不重复的移到前面,返回去重后的尾部迭代器。通常配合 erase 使用。

    cpp 复制代码
    vector<int> v = {1, 1, 2, 3, 3};
    auto last = unique(v.begin(), v.end());
    v.erase(last, v.end()); // 真正删除多余元素
    // v 变为: {1, 2, 3}

8.返回二分查找边界

  • lower_bound / upper_bound

    • 作用:二分查找边界。

      • lower_bound: 找第一个 ≥val 的位置。
      • upper_bound: 找第一个 >val 的位置。
    cpp 复制代码
    vector<int> v = {1, 2, 4, 4, 5};
    // 找第一个 >= 4 的位置
    auto pos = lower_bound(v.begin(), v.end(), 4);
    cout << pos - v.begin(); // 输出: 2 (下标)

质数筛 欧拉筛

cpp 复制代码
const int N = 1e8 + 5; // 根据需要调整上限
bool vis[N];           // 标记数组,true表示合数
int primes[N], cnt;    // 存储素数及计数

void euler_sieve(int n) {
    for (int i = 2; i <= n; ++i) {
        if (!vis[i]) primes[++cnt] = i; // 未被标记即为素数
        
        for (int j = 1; j <= cnt && i * primes[j] <= n; ++j) {
            vis[i * primes[j]] = true;  // 标记合数
            if (i % primes[j] == 0) break; // 【核心】保证每个合数只被最小质因数筛除
        }
    }
}

最小公倍数 最大公约数

cpp 复制代码
// 求最大公约数
int gcd(int a, int b) {
    return b == 0 ? a : gcd(b, a % b);
}

// 求最小公倍数(防溢出写法)
long long lcm(long long a, long long b) {
    return a / gcd(a, b) * b;
}

矩阵

1.矩阵乘法

cpp 复制代码
// 假设 A 为 m*s, B 为 s*n, 结果存入 C (m*n)
void matrixMul(const vector<vector<int>>& A, const vector<vector<int>>& B, vector<vector<int>>& C) {
    int m = A.size(), s = A[0].size(), n = B[0].size();
    C.assign(m, vector<int>(n, 0)); // 初始化结果矩阵
    
    for (int i = 0; i < m; ++i) {
        for (int j = 0; j < n; ++j) {
            for (int k = 0; k < s; ++k) {
                C[i][j] += A[i][k] * B[k][j];
            }
        }
    }
}

2. 矩阵镜像 / 翻转 (Mirror / Flip)

cpp 复制代码
// 原地垂直镜像(左右翻转)
void flipVertical(vector<vector<int>>& mat) {
    int m = mat.size(), n = mat[0].size();
    for (int i = 0; i < m; ++i) {
        for (int j = 0; j < n / 2; ++j) {
            swap(mat[i][j], mat[i][n - 1 - j]);
        }
    }
}

// 原地水平镜像(上下翻转)
void flipHorizontal(vector<vector<int>>& mat) {
    int m = mat.size();
    for (int i = 0; i < m / 2; ++i) {
        swap(mat[i], mat[m - 1 - i]); // 直接交换整行
    }
}

3. 矩阵旋转 (Rotate)

cpp 复制代码
// 原地顺时针旋转 90 度
void rotate90Clockwise(vector<vector<int>>& mat) {
    int n = mat.size();
    
    // 步骤1:转置矩阵
    for (int i = 0; i < n; ++i) {
        for (int j = i + 1; j < n; ++j) { // j从i+1开始避免重复交换
            swap(mat[i][j], mat[j][i]);
        }
    }
    
    // 步骤2:翻转每一行
    for (int i = 0; i < n; ++i) {
        reverse(mat[i].begin(), mat[i].end());
    }
}

4. 对称矩阵 (Symmetric Matrix)

cpp 复制代码
// 判断是否为对称矩阵
bool isSymmetric(const vector<vector<int>>& mat) {
    int n = mat.size();
    for (int i = 0; i < n; ++i) {
        for (int j = 0; j < i; ++j) { // 只需遍历下三角或上三角即可
            if (mat[i][j] != mat[j][i]) return false;
        }
    }
    return true;
}

5.蛇形矩阵

字符串处理

1. 字符串转数值 (STO系列函数)

自 C++11 标准起,C++ 提供了一系列以 sto (string to...) 开头的函数,用于安全地将字符串转换为对应的数值类型:

  • stoi(): string to int(转为整型)
  • stol(): string to long(转为长整型)
  • stoll(): string to long long(转为长长整型)
  • stoul() / stoull(): 转为无符号长整型/无符号长长整型
  • stof(): string to float(转为单精度浮点型)
  • stod(): string to double(转为双精度浮点型)
  • stold(): string to long double(转为长双精度浮点型)

2. 数值转字符串 (to_string)

3. 截取与复制 (Substring & Copy)

  • substr(pos, len) :提取子串。从索引 pos 开始,截取长度为 len 的子串并返回一个新的 string。如果省略 len,则默认截取到字符串末尾。
  • copy(char* s, size_t n, size_t pos) :将 string 中的字符序列拷贝到指定的 C 风格字符数组中(注意:它不会自动追加 \0)。

4. 查找与定位 (Find)

  • find(str/char, pos):从前往后查找子串或字符首次出现的位置。
  • rfind(str/char, pos):从后往前查找最后一次出现的位置。
  • find_first_of(chars):查找目标字符集中任意字符首次出现的位置。
  • find_last_of(chars):查找目标字符集中任意字符最后出现的位置。
  • find_first_not_of(chars) / find_last_not_of(chars) :查找首次/最后一次 不在 目标字符集中的字符位置。

5. 修改与拼接 (Modify & Append)

  • append(str) / operator+=:在字符串末尾追加内容,两者功能基本一致。
  • push_back(c):在字符串尾部追加单个字符。
  • insert(pos, str):在指定位置插入新的字符串或字符。
  • erase(pos, len) :删除从 pos 开始的 len 个字符。
  • replace(pos, len, str):替换指定位置的子串。
  • pop_back():删除字符串末尾的最后一个字符。
  • clear():清空整个字符串。

6. 容量与状态查询 (Capacity & Status)

  • size() / length() :获取字符串的有效字符长度,两者完全等价(推荐使用 size() 以保持与 STL 容器命名一致)。
  • empty() :判断字符串是否为空,比 size() == 0 更直观。
  • capacity():获取当前分配的内存容量。
  • reserve(n) :预分配至少 n 个字符的内存空间,常用于循环追加前避免频繁扩容。

7. 比较操作 (Compare)

  • compare(str) :按字典序比较两个字符串。返回 0 表示相等,正数表示当前字符串更大,负数表示更小。
  • 运算符重载 :C++ 的 string 直接支持 ==, !=, <, >, <=, >= 进行字典序比较,日常开发中直接使用运算符即可。

特殊输入方式

这道题是一次输入一行,但是要转为二维数组。

可以用char temp定义临时字符,然后每次输入都只接受一个字节的数据,然后就可以用temp-'0'转换了。

cpp 复制代码
    cin>>n>>m;
    char temp;
    for(int i=1;i<=n;i++){
        for(int j=1;j<=m;j++){
            cin>>temp;
            maze[i][j]=temp-'0';
        }
    }
cpp 复制代码
char temp;
for(int i=1;i<=N;i++){
        for(int j=1;j<=M;j++){
            cin>>temp;
            if(temp=='#'){
                maze[i][j]=0;
            }else{
                maze[i][j]=1;
            }
        }
    }

带有空格的整行输入要用getline(cin,s)否则会截断

如果题目给定的后缀表达式非常长,或者中间带有空格换行,cin >> s 遇到空格就会停止读取,导致后面的数字和运算符全部丢失。必须使用 getline(cin, s)

混合数组和字符的输入处理

cpp 复制代码
    string s;
    getline(cin, s);
    stack<long long> st;
    string num = "";
    for (char c : s) {
        //第一个可能是数字也可能是负号,要完整接受一个多位数字:先通过一次性收集齐"."前的字符,然后一次stod变成数字
        if (isdigit(c)) {
            num += c;
        }

特殊的输出方式

要求带精度小数的输出

cpp 复制代码
cout<<fixed<<setprecision(2)<<curv<<endl;

注意 拓扑问题一个节点可以有多个输出边,因此要一直输入

cpp 复制代码
    for(int i=1;i<=N;i++){
        //注意 拓扑问题一个节点可以有多个输出边,因此要一直输入
        while(cin>>v && v){
            cin>>v;
            addEdge(i,v);
            inE[v]++;
        }
    }

排序

桶排序 T407375

https://www.luogu.com.cn/problem/T407375

用vector开桶,计算桶的大小,桶大小=max-min+1/N,

然后输入到各个桶,idx=(ai-min)/N; if(idx==N) idx=N-1; pushback(ai)

cpp 复制代码
#include<bits/stdc++.h>
using namespace std;
int N;

int a[100005];
//开N个桶,涉及范围max min,单个桶的大小就是range=max-min+1 / N
int main(){
    vector<int> buk[100005];
    cin>>N;
    int min_n=(int)1e9, max_n=0;
    for(int i=0;i<N;i++){
        cin>>a[i];
        max_n=max(max_n,a[i]);
        min_n=min(min_n,a[i]);
    }
    long long range = (long long)max_n - min_n + 1;
    double bk_size = (double)range / N; 
    for(int i=0;i<N;i++){
        
        int idx=(a[i]-min_n)/bk_size;//落入区间是当前值-最小值/桶大小
        if(idx >= N) idx = N - 1; 
        buk[idx].push_back(a[i]);
    }
    for(int i=0;i<N;i++){
        //对各个桶排
        sort(buk[i].begin(),buk[i].end());
        for(int x:buk[i]){
            cout<<x<<" ";
        }
    }
    
}

链表

创建链表

可以用Node *head在堆上创建,这种需要dlete删除。

也可以用Node head;

head.val=10;

head->next=&node1;(指针必须用->访问)

遍历链表

初始节点指向head->next(第一个有值),然后通过cur->val遍历访问。

插入节点(p后面插一个)

new新节点,填值,当前下一个=原先下一个,原先下一个=当前

删除节点(p的下一个)

暂存当前节点,指向下一个的下一个,删除d

查找节点

模板

cpp 复制代码
#include<iostream>
using namespace std;
struct Node{
    int val;
    Node* next;
};

void insert(Node* p,int val){
    Node*q = new Node();
    q->val = val;
    q->next = p->next;
    p->next = q;
}

Node* findf(Node* head, int target){
    Node* cur = head->next;
    while(cur!=NULL && cur->val!=target){
        cur=cur->next;
    }
    return cur;
}
void printn(Node* head){
    Node* cur = head->next;
    while(cur!=NULL){
        cout<<cur->val<<" ";
        cur=cur->next;
    }
}
void deletep(Node* head,int tar){
    Node* cur = head;
    while(cur!=NULL && cur->next!=NULL){
        if(cur->next->val==tar){
            Node* d = cur->next;
            cur->next=cur->next->next;
            delete d;
        }else{
            cur=cur->next;
        }
    }
    
}

int main(){
    Node* head=new Node();
    Node* n1 = new Node();
    n1->val=5;
    head->next=n1;
    Node* n2 = new Node();
    n2->val=10;
    n1->next=n2;
    Node* n3 = new Node();
    n2->next=n3;
    n3->val=15;

    insert(head,10);
    insert(head,23);
    printn(head);
    cout<<endl;
    Node* tar = findf(head,5);
    insert(tar,11);
    deletep(head,10);

    printn(head);
    cout<<endl;
    return 0;
}

静态链表(数组模拟)

创建链表

遍历链表

插入链表

删除链表

查找链表

模板

cpp 复制代码
/*每个结点存一个int型数据
创建一个头结点不存数据的空链表
创建三个结点,数值分别为5、10、15
5->10->15
在链表头部插入数值为10的结点
在链表头部插入数值为23的结点
依次输出当前链表的值,用空格隔开,输出所有值后换行
找到链表值为5的结点,在其后插入一个值为11的结点
删除所有值为10的结点
依次输出当前链表的值,用空格隔开,输出所有值后换行*/
#include<iostream>
using namespace std;
struct Node {
    int val;
    int next;
};
Node nodes[1000];
int cnt = 1;
void insert(int pre, int val) {//在pre后节点加一个
    nodes[cnt].val = val;
    nodes[cnt].next = nodes[pre].next;
    nodes[pre].next = cnt;
    cnt++;
}
void printn(int head) {
    int cur = nodes[head].next;
    while (cur) {
        cout << nodes[cur].val << " ";
        cur = nodes[cur].next;
    }
    cout << endl;
}
int findn(int head, int tar) {
    int cur = head;
    while (nodes[cur].next != 0 && nodes[nodes[cur].next].val != tar) {
        cur = nodes[cur].next;//一定不是自然递增,索引不连续
    }
    return nodes[cur].next;//返回找到的本届点
}

void deleteAll(int head, int tar) {
    int cur = head;
    while (nodes[cur].next != 0) {//其实不用判定当前是否为零,因为从head开始的地址就不为0
        if (nodes[nodes[cur].next].val == tar) {
            nodes[cur].next = nodes[nodes[cur].next].next;
        }
        else {
            cur = nodes[cur].next;
        }
    }
}

int main() {
    int head = 0;
    insert(head, 15);
    insert(head, 10);
    insert(head, 5);


    insert(head, 10);
    insert(head, 23);
    printn(head);
    int f = findn(head, 5);
    insert(f, 11);
    deleteAll(head, 10);
    printn(head);
    return 0;
}

约瑟夫问题 P1996

https://www.luogu.com.cn/problem/P1996

cpp 复制代码
#include<bits/stdc++.h>
using namespace std;

//维护一个下标 一个当前计数值 下标用于删除和输出 当前计数值用于判断m 一个剩余数量
int cur=1,cnt=0,rem;

int pp[1005];
int main(){
    int n,m;
    cin>>n>>m;
    //cur是下标 cnt是计数值 每次cur==m都会触发清零 cnt==0;
    //当剩余数量
    rem=n;
    while(rem){
        if(pp[cur]==0){//如果当前人活着
            cnt++;//计数++
            if(cnt==m){//如果这个人正好是cnt第m个就杀了
                cout<<cur<<" ";
                cnt=0;//清零计数
                pp[cur]=1;//标记死人
                rem--;//剩余人数--
            }
        }
        cur++;
        if(cur>n) cur=1;
    }
}

队列

入队

头往后

出队

尾巴往前移

环形队列

STL队列 queue

STL双端队列 deque

单调队列(找各区间最大值)

维护一个时间窗口,先检查队头是否过期,如果过期就弹出,然后检查队尾如果比当前小就弹出,然后入队。

优先队列

STL栈

前中后缀表达式

中缀转前缀

中缀转后缀

前后缀表达式的计算

前缀表达式的计算

前缀从右往左压入栈,遇到运算符弹栈计算。

后缀表达式的计算

后缀从左往右压入栈,遇到运算发弹栈计算。注意顺序是次栈顶<运算>栈顶。

练习 P1449

https://www.luogu.com.cn/problem/P1449

cpp 复制代码
#include <iostream>
#include <stack>
#include <string>
using namespace std;
//
int main() {
    string s;
    getline(cin, s);
    stack<long long> st;
    string num = "";
    for (char c : s) {
        //第一个可能是数字也可能是负号,要完整接受一个多位数字:先通过一次性收集齐"."前的字符,然后一次stod变成数字
        if (isdigit(c)) {
            num += c;
        }
        else if (c == '.') {//如果遇到是.就std
            st.push(stoll(num));
            num.clear();//输入完要清空
        }
        else if (c == '@') {
            break;
        }
        else {
            long long op2 = st.top(); st.pop();
            long long op1 = st.top(); st.pop();

            if (c == '+') {
                //cout << op1 <<"+"<< op2 <<"="<<op1+op2 << endl;
                st.push(op1 + op2);

            }
            else if (c == '-') {
                //cout << op1 << "-" << op2 << "=" << op1 - op2 << endl;
                st.push(op1 - op2);

            }
            else if (c == '*') {
                //cout << op1 << "*" << op2 << "=" << op1 * op2 << endl;
                st.push(op1 * op2);

            }
            else {
                //cout << op1 << "/" << op2 << "=" << op1 / op2 << endl;
                st.push(op1 / op2);

            }
        }


    }
    cout << st.top() << endl;
}

单调栈(下一个比自己大的元素 NGE问题)

牛只能看到之前比自己高的牛,比如53214 对于5号牛,只能看到5.对于4号牛能看到532,是一个单调递减栈。每次只需要pop调比自己矮的牛,那么s.size()就是能看到的数量了。

STL

排序sort()

正序

cpp 复制代码
sort(num.begin(), num.end()); 

逆序

cpp 复制代码
// rbegin() 指向最后一个元素,rend() 指向第一个元素的前一个位置
sort(num.rbegin(), num.rend()); 

自定义类型使用sort()

如果 cmp(a, b) 返回 truesort 就认为 a 应该排在 b 的前面。

如果 cmp(a, b) 返回 falsesort 就认为 a 应该排在 b 的后面(或者顺序不变)。

第二关键词排序

cpp 复制代码
// 比较函数:优先按时间升序;时间相同时,按原始编号(idx)升序
bool cmp(const Node& a, const Node& b) {
    if (a.t != b.t) return a.t < b.t;
    return a.idx < b.idx;
}

精度限制输出

cpp 复制代码
cout << fixed << setprecision(10) << ans << endl;

清空/初始化 Memset

对象、多少、大小sizeof

cpp 复制代码
memset(arr, 0, sizeof(arr)); // 将整个数组的每个字节设为0

容器

顺序容器:通过位置来访存元素

容器适配器:集成了基本容器的容器

关联容器:字典,用于表达键值对关系

无序容器:哈希表

Vector

vector初始化长度后,填充不可以用push_back(),否则只是往后加,必须用veci填充。

如果没有初始化长度才可以用push_back().

vector开在main外面。

二维vector

矩形二维vector

可变长一维数组的拼接 与 创建 遍历

这里例子每一行是i的大小递增。

如果要求任意长度,可以用resize(),再push_back进vec4中。

对Vector的遍历

一种是

Resize 操作 例题

带初始值的resize,将行数扩到4,第四行是全0的大小为4的vector

cpp 复制代码
std::vector<std::vector<int>> matrix(3, std::vector<int>(3, 0)); // 初始为 3x3

// 将行数扩容到 4,新增的第4行是一个包含 4 个元素且全为 0 的 vector
matrix.resize(4, std::vector<int>(4, 0)); 

不带初始值的resize,将行数扩到4,第四行是null,在使用的时候必须对列resize。

cpp 复制代码
std::vector<std::vector<int>> matrix(3, std::vector<int>(3, 0)); // 初始为 3x3

// ❌ 危险操作:行数变成了 4,但第 4 行(matrix[3])里面没有分配任何空间!
matrix.resize(4); 

P3613

https://www.luogu.com.cn/problem/P3613

cpp 复制代码
#include<iostream>
#include<vector>
using namespace std;
vector<vector<int>> store;

int main(){
    int n,q;
    cin>>n>>q;
    store.resize(n+1);
    int p,i,j,k;
    for(int v=0;v<q;v++){
        cin>>p;
        if(p==1){
            cin>>i>>j>>k;
            if(store[i].size()<=j){store[i].resize(j+1);}
            store[i][j]=k;
        }else{
            cin>>i>>j;
            cout<<store[i][j]<<endl;
        }
    }
    return 0;
}

List

List的遍历

实际上直接for(auto x:lst)即可。

List的查找

找第一个/找所有的val都可以

可以直接用algorithm的函数

find(begin,end,val)

List的删除

删除单个节点

erase(find(10))

删除所有节点

remove(10)

List的插入

在后面插入 push_back

在前面插入 push_front

在i之前插入

L.insert(i,100)

在i之后插入

auto i=find(5)

i++

L.insert(i,1000)

翻转链表

List与Vector的转换

1. Vector转List

cpp 复制代码
std::vector<int> myVec = {1, 2, 3, 4};
// 利用迭代器范围直接构造 list
std::list<int> myList(myVec.begin(), myVec.end()); 

2. List转Vector

这是最直接的方法。std::vector 提供了一个接受迭代器范围的构造函数,可以直接接收 std::list 的头尾迭代器进行初始化:

cpp 复制代码
std::list<int> myList = {5, 6, 7, 8};
// 利用迭代器范围直接构造 vector
std::vector<int> myVec(myList.begin(), myList.end()); 

应用

快排回去

List排序很慢

cpp 复制代码
std::list<int> myList = {...}; // 假设包含大量无序数据

// 1. 将 list 转换为 vector
std::vector<int> tempVec(myList.begin(), myList.end()); 

// 2. 对 vector 进行高效排序
std::sort(tempVec.begin(), tempVec.end()); 

// 3. 将排好序的数据拷贝回 list
10myList.assign(tempVec.begin(), tempVec.end()); 

Map 字典(增删改查 且有不重复唯一键 带值去重)

不支持随机访问,必须用it迭代器

会对key建立索引。

如果是自定义类型的key,必须实现<

基本用法

find返回的是迭代器,如果是尾后迭代器就是没找到。

遍历的时候需要用i->first ->second输出元素。

练习

T424396

https://www.luogu.com.cn/problem/T424596

P1918

https://www.luogu.com.cn/problem/P1918

Set(增删改查 且本身就是可作为索引 唯一 去重)

不支持随机访问 必须用it迭代。

注意,set是有序的,是可以建立索引和遍历的。集合是无序的。

注意,set的插入是insert。

set的遍历

输出是有序的 这里插入10 15 11但是输出是唯一且有序的 7 8 10 11 15.

练习

带重复的第k小整数,但是要求不重复的顺序。最核心的痛点就是去重。

注意,set不支持随机访问,必须顺序遍历

cpp 复制代码
//迭代器不可以跳变,只能一个个叠加上去。
    auto it=s.begin();
    for(int i=0;i<k-1;i++){
        it++;
    }
cpp 复制代码
#include<bits/stdc++.h>
using namespace std;

set<int> s;
int main(){
    int n,k;
    cin>>n>>k;
    for(int i=0;i<n;i++){
        int temp;
        cin>>temp;
        s.insert(temp);
    }
    if(k > s.size()){
        cout << "NO RESULT" << endl;
        return 0;
    }
    //迭代器不可以跳变,只能一个个叠加上去。
    auto it=s.begin();
    for(int i=0;i<k-1;i++){
        it++;
    }
    cout<<*it<<endl;
}

set和Map的upper_bound和lower_bound

当查找基于红黑树实现的容器,比如set和map,都可以用bound函数找到第一个满足大于这个值/小于这个值的值。

P5250

https://www.luogu.com.cn/problem/P5250

正确解法:利用 lower_bound

std::set 内部是红黑树,支持 O(log⁡N)的查找。我们可以利用 s.lower_bound(val)

  • 它会返回第一个 大于等于 val 的元素的迭代器。

策略:

  1. 精确查找 :先 find(val),如果有直接取走。
  2. 模糊查找
    • lower_bound(val) 找到第一个≥val 的数,记为 it_high(候选1:偏大的)。
    • 如果 it_high 不是 begin(),那么 it_high 的前一个数 prev(it_high) 一定是 <val 的最大数,记为 it_low(候选2:偏小的)。
    • 比较这两个候选者与 val 的差值。题目要求"一样接近取较短",意味着如果差值相等,优先取 it_low

找第一个大于等于val的迭代器

cpp 复制代码
auto it_high=s.lower_bound(val)

然后就可以用

cpp 复制代码
*(--it_high)

表达后一个

cpp 复制代码
*(++it_high)
cpp 复制代码
#include<bits/stdc++.h>
using namespace std;

set<int> s;
//vector<int> vec;
int main(){
    int m;
    cin>>m;
    for(int i=0;i<m;i++){
        int op=0;
        cin>>op;
        if(op==1){
            int val=0;
            cin>>val;
            auto it=s.find(val);
            if(it==s.end()){//如果没招到
                s.insert(val);
            }else{
                cout<<"Already Exist"<<endl;
            }
        }else if(op==2){
            int val=0;
            cin>>val;
            auto it=s.find(val);
            if(s.empty()){
                cout<<"Empty"<<endl;
            }else if(it!=s.end()){
                s.erase(val);
                cout<<val<<endl;
            }else{//如果找不到长度一样的,就找最小的 直接上二分
                auto it_high=s.lower_bound(val);//去找第一个满足大于val的迭代器
                int chose=0;
                if(it_high==s.end()){//如果所有的都不满足,说明都太短了
                    chose=*(--it_high);
                }else if(it_high==s.begin()){//所有的都大于,都太长了
                    chose=*it_high;
                }else{
                    int v_high=*it_high;
                    int v_low=*(--it_high);

                    if(v_high-val<val-v_low){//更近就选更近的,一样就选短的
                        chose=v_high;
                    }else{
                        chose=v_low;
                    }

                    
                }
                s.erase(chose);
                cout<<chose<<endl;
            }
            
            
        }
    }
}

哈希(超大范围的不重复统计)不咋考

一般都需要offset偏移量

需要将一个大范围压缩到一个小范围时,比如key1的值是-1亿,key4是一亿,就需要映射到小桶中。因为数组不能开1e7的桶。

特性 std::map unordered_map (Hash)
底层结构 红黑树 (平衡二叉树) 哈希表 (数组+链表)
查找/插入速度 logN 1
数据顺序 自动排序 (从小到大) 无序 (乱序)
内存占用 较小 (每个节点只需存左右孩子指针) 很大 (需要预留大量空桶以防冲突)
支持有序访问

Unordered_Map和Unordered_Set

二分查找(寻找一个数/最大值最小值 必须有序)

只分第一次和最后一次满足。

整数二分

二分满足答案

所有check(p)类的问题都基本满足第一类,只要是递增关系,都是第一个大于T的。

二分一定要分区**"第一次满足""最后一次满足"**

比如最后一次满足,说明是找区间右侧的值,要压缩左边界。

比如**第一次满足,**说明是找区间左侧的值,要压缩右边界。

要不你把y>=s y>s y<=s y<s的四种情况都说一下吧

二分法核心

只有两种情况,一种是第一次满足,一种是最后一次满足

这种诡异的模板实际上是 保留法和排除法的混合。

做题应该根据求第一个还是最后一个,然后锁边界/输出。

练习 矿石问题

P1314

https://www.luogu.com.cn/problem/P1314

cpp 复制代码
#include<bits/stdc++.h>
using namespace std;
int n,m;
long long s;
//提到了区间和 就是满足某个性质的区间和问题
//重量和价值都会影响区间和,因此需要开两个presum前缀和数组,presum一定要对应一个原始数组,因为迭代的时候肯定要用到presum[i]=presum[i-1]+a[i]
long long presumw[2000005];
long long presumv[2000005];
long long w[2000005];
long long v[2000005];
//同时还要添加检验区间
long long intL[2000005],intR[2000005];
void init(){
    memset(presumw,0,sizeof(presumw));
    memset(presumv,0,sizeof(presumv));
}

long long caly(long long W){
    init();
    for(int i=1;i<=n;i++){
        
        if(w[i]>=W){
            //弄presum
            presumw[i]=presumw[i-1]+1;
            //注意 这里的式子是01,所以是+1不是+w[i]!!!
            presumv[i]=presumv[i-1]+v[i];
        }else{//带性质判断的特殊点就在于 不满足性质的时候也要写,因为是迭代式,不写就断了,这里保持上一个状态即可。
            presumw[i]=presumw[i-1];
            presumv[i]=presumv[i-1];
        }
    }
    //检测M组区间的所有满足性质的值
    long long Y=0;
    for(int i=1;i<=m;i++){
        long long L=intL[i],R=intR[i];
        Y+=(presumw[R]-presumw[L-1])*(presumv[R]-presumv[L-1]);
    }
    return Y;
}
bool check(long long W){
    return caly(W)>=s;
}
int find(){
    long long L=0,R=1e12;
    
    while(R>L){
        long long W=(R+L+1)/2;
        if(caly(W)>=s){//最后一个满足y>=s的 最后一个 L=mid
            L=W;
        }else{
            R=W-1;
        }
    }
    // while(R>L){
    //     long long W=(R+L)/2;
    //     if(caly(W)<s){//第一个满足y<s的 第一个 R=mid
    //         R=W;
    //     }else{
    //         L=W+1;
    //     }
    // }
    //上面输出的是第一个大于s的W 因此应该检查W和W+1的输出
    if(abs(caly(L)-s)<abs(caly(L+1)-s)){
        cout<<abs(caly(L)-s)<<endl;
        return L;
    }else{
        cout<<abs(caly(L+1)-s)<<endl;
        return L+1;
    }

}
int main(){
    cin>>n>>m>>s;
    for(int i=1;i<=n;i++){
        cin>>w[i]>>v[i];
    }
    for(int i=1;i<=m;i++){
        cin>>intL[i]>>intR[i];
    }

    long long result=find();
    //cout<<result<<endl;
    return 0;
}

模板

见上文。

练习

复印机

SDUOJ

//二分变量是时间 T

//判断时间单调性

//确定搜索范围 l r l=0 r=n*min(x,y)

//批准条件check 能在T内完成复印n-1份

二分条件判断:

找到第一个满足if()的,

cpp 复制代码
while(R>L){
        int mid=(L+R)/2;
        if(check(mid)){//如果复印完 时间多
            R=mid;
        }else{
            L=mid+1;
        }
    }
cpp 复制代码
#include<bits/stdc++.h>
using namespace std;
//二分变量是时间 T
//判断时间单调性
//确定搜索范围 l r l=0 r=n*min(x,y)
//批准条件check 完成复印
int x,n,y;
long long max_t=1e9;
bool check(int T){//这个时间是否能复印完n-1件
    long long cnt=T/x+T/y;
    return cnt>=(n-1);
}
int find(){
    long long L=0,R=max_t;
    while(R>L){
        int mid=(L+R)/2;
        if(check(mid)){//如果复印完 时间多
            R=mid;
        }else{
            L=mid+1;
        }
    }
    return L;
}
int main(){
    cin>>n>>x>>y;
    //max_t=n*min(x,y);
    if(n==1) cout<<0<<endl;
    if(n==2){
        cout<<min(x,y)<<endl;
    }else{
    //其他情况只需要复印n-1份,然后有一份直接单独印刷
        long long min_t=find();
        cout<<min_t+min(x,y)<<endl;
    }
    
    return 0;
}

浮点二分(解方程 也需要单调)

方法五步:

①判断搜索变量

②变量是否单调

③const double eps=1e-6 确定误差

④double/int l r 确定搜索范围(可减小)

⑤批准条件check()

一定要注意,是否所有的参与变量都使用double了》

可以预设解的范围缩小

模板

将if(f(mid)*f(r)<=0)换为bool check()函数

用次数截止(查找多少次停止)

练习

SDUOJ

截绳子

cpp 复制代码
#include<bits/stdc++.h>
using namespace std;
//可行性判断 绳子长度是可单调的
//double len=(r-l)/2 变量是绳子的长度
//eps 误差是10e-6
//double l r 查找范围是 1,1e7
//批准条件check() 批准条件是能切出k段这么长的绳子
const double eps=1e-6;
int n,k;
vector<int> rope(1e7);
bool check(double len){
    int cnt=0;
    for(int x:rope){
        if(x>=len){//如果能够切
            cnt+=x/len;
        }else{
            break;
        }
    }
    return cnt>=k;//能切够k段吗
}
double find(){
    double l=0,r=1e7;
    while(abs(r-l)>eps){
        double len=(l+r)/2;
        if(check(len)){//如果够切就可以增加长度 
            l=len;
        }else{
            r=len;
        }
    }
    return l;
}

int main(){
    
    cin>>n>>k;
    for(int i=0;i<n;i++){
        cin>>rope[i];
    }
    sort(rope.rbegin(),rope.rend());
    double max_len=find();
    cout<<max_len<<endl;
    return 0;
}

前缀和(用于计算区间和 出现Σ都先考虑前缀和)

得到前缀和关系,计算区间和

方法两步:

①创建原数组 和 presum数组(从1开始)

②sumi=sumi-1+ai; 得到前缀和关系

②输出sumr-suml-1 输出区间和

一维前缀和

模板 P8218

https://www.luogu.com.cn/problem/P8218

cpp 复制代码
#include<bits/stdc++.h>
using namespace std;
int a[1000005],sum[1000005];
int main(){
    int n;
    cin>>n;
    for(int i=1;i<=n;i++){
        cin>>a[i];
        sum[i]=a[i]+sum[i-1];
    }
    int m;
    cin>>m;
    for(int i=0;i<m;i++){
        int r,l;
        cin>>l>>r;
        cout<<sum[r]-sum[l-1]<<endl;
    }
    return 0;
}

练习

P1614

https://www.luogu.com.cn/problem/P1614

cpp 复制代码
#include<bits/stdc++.h>
#include<limits.h>
using namespace std;
int a[3005],presum[3005];

int main(){
    int n,m;
    cin>>n>>m;
    for(int i=1;i<=n;i++){
        cin>>a[i];
        presum[i]=presum[i-1]+a[i];
    }
    int minn=INT_MAX;
    for(int i=0;i<=n-m;i++){
        minn=min(minn,(presum[i+m]-presum[i]));
    }
    cout<<minn;
    return 0;
}

二维前缀和

由于会涉及fij=fi-1j+fij-1-fi-1j-1+aij; 因此循环必须从1开始到n

模板 P1719

https://www.luogu.com.cn/problem/P1719

cpp 复制代码
#include<bits/stdc++.h>
#include<limits.h>
int a[125][125],f[125][125];
using namespace std;
int main(){
    int n;
    cin>>n;
    for(int i=1;i<=n;i++){
        for(int j=1;j<=n;j++){
            cin>>a[i][j];
            f[i][j]=f[i-1][j]+f[i][j-1]-f[i-1][j-1]+a[i][j];
        }
    }
    int ans=INT_MIN;
    for(int a=1;a<=n;a++){
        for(int b=1;b<=n;b++){
            for(int c=a;c<=n;c++){
                for(int d=b;d<=n;d++){
                    int cur=f[c][d]+f[a-1][b-1]-f[c][b-1]-f[a-1][d];
                    ans=(ans>cur?ans:(cur));
                }
            }
        }
    }
    cout<<ans<<endl;
    return 0;
}

练习

P2004

https://www.luogu.com.cn/problem/P2004

cpp 复制代码
#include<bits/stdc++.h>
#include<limits.h>
using namespace std;
int a[3000][3000],presum[3000][3000];
int main(){
    int n,m,c;
    cin>>n>>m>>c;
    for(int i=1;i<=n;i++){
        for(int j=1;j<=m;j++){
            cin>>a[i][j];
            presum[i][j]=a[i][j]-presum[i-1][j-1]+presum[i-1][j]+presum[i][j-1];
        }
    }
    int val=INT_MIN;
    int max=INT_MIN;
    int cptx=1,cpty=1;
    //就是这里全减去一了?
    for(int i=1; i<=n-c+1; i++){
        for(int j=1; j<=m-c+1; j++){
            val = presum[i+c-1][j+c-1] 
                  + presum[i-1][j-1] 
                  - presum[i-1][j+c-1] 
                  - presum[i+c-1][j-1];
            if(val>max){
                cptx=i;
                cpty=j;
                max=val;
            }
        }
    }
    cout<<cptx<<" "<<cpty<<endl;
    return 0;
}

差分(用于区间修改)

也可以求 区间内满足某个性质的数有多少个

方法一步:

坑前加 坑后补

cpp 复制代码
    for(int i=0;i<p;i++){
        cin>>x>>y>>z;
        diff[x]+=z;
        //if(y+1<=n)
        diff[y+1]-=z;
    }

一维差分

差分和前缀和就是离散的积分和求导。

可以先差分后前缀和 也可以先前缀和再差分。

模板 P2367

https://www.luogu.com.cn/problem/P2367

差分是导数,所以要求变化后的数组需要计算变化量,变化量就是对导数积分,也就是对差分求前缀和。presum的值就是变化量,此时再把presum加回a数组即可得到变化后的数组。

cpp 复制代码
for(int i=1;i<=n;i++){
        presum[i]=presum[i-1]+diff[i];
        a[i]+=presum[i];
        min=(a[i]>min)?min:a[i];
    }
cpp 复制代码
#include<bits/stdc++.h>
#include<limits.h>
using namespace std;
int stu[5000005];
int diff[5000005]={0};
int presum[5000005]={0};
int n,p;
int main(){

    cin>>n>>p;
    
    for(int i=1;i<=n;i++){
        cin>>stu[i];
    }
    int x,y,z;
    for(int i=0;i<p;i++){
        cin>>x>>y>>z;
        diff[x]+=z;
        //if(y+1<=n)
        diff[y+1]-=z;
    }
    int min=INT_MAX;
    for(int i=1;i<=n;i++){
        presum[i]=presum[i-1]+diff[i];
        stu[i]+=presum[i];
        min=(stu[i]>min)?min:stu[i];
    }
    cout<<min<<endl;
    return 0;
}

练习

P3406

https://www.luogu.com.cn/problem/P3406

这题首先要看出是区间修改问题,然后要注意,得到的是城市的终点起点,而要累加的是路径,因此是比城市少一个的(两城市间一条路),所以在写差分时是startend而不是end+1

cpp 复制代码
#include<bits/stdc++.h>
using namespace std;

long long diff[100005];
//要看出是区间修改问题,每次从任意两个站点走,因此中间段都要+1,是区间修改
int main(){
    int N,M;
    cin>>N>>M;
    int p1,p2;
    cin>>p2;
    for(int i=1;i<M;i++){
        cin>>p1;
        int start =min(p1,p2),end=max(p1,p2);
        diff[start]++,diff[end]--;//为什么是end就减一 而不是end+1
        p2=p1;
    }
    long long a,b,c;
    long long num=0,money=0;
    for(int i=1;i<N;i++){
        cin>>a>>b>>c;
        num+=diff[i];//前缀和获得修改后的变化量,也就是各段乘坐的次数
        money+=min(num*a,num*b+c);//注意买卡只需要买一次,后续都可以用b的价格坐
    }
    cout << money << endl;

    return 0;
}

P2697

https://www.luogu.com.cn/problem/P2697

注意偏移下标是为了映射初次出现到负数域,如果不用偏移,那么下标恒为正无法映射负数。

偏移后必须同步拓展数组。

cpp 复制代码
#include<bits/stdc++.h>
using namespace std;
int emerg[2000005];
int presum[2000005];
int OFFSET=1e6;
int main(){
    memset(emerg,-1,sizeof(emerg));
    string s;
    cin>>s;
    int max_len=0;
    for(int i=1;i<=s.length();i++){
        if(s[i-1]=='G') {
            presum[i]=presum[i-1]+1;
        }else{
            presum[i]=presum[i-1]-1;
        }
        int idx=presum[i]+OFFSET;

        if(presum[i]==0){//因为这一定是当前最长的串
            max_len=i;
        }
        if(emerg[idx]!=-1){//说明是第二次出现
            max_len=max(max_len,i-emerg[idx]);
        }else{//说明是第一次出现
            emerg[idx]=i;//设置为第一次出现的下标
        }
    }
    cout<<max_len<<endl;
    return 0;
}

二维差分

模板 P3397

https://www.luogu.com.cn/problem/P3397

cpp 复制代码
#include<bits/stdc++.h>
using namespace std;
int arr[1005][1005];
int diff[1005][1005];
int presum[1005][1005];
int n,m;
int main(){
    
    cin>>n>>m;
    
    for(int i=0;i<m;i++){
        int a,b,c,d;
        cin>>a>>b>>c>>d;
        diff[a][b]++;
        diff[c+1][d+1]++;
        diff[a][d+1]--;
        diff[c+1][b]--;
    }
    for(int i=1;i<=n;i++){
        for(int j=1;j<=n;j++){
            presum[i][j]=diff[i][j]+presum[i-1][j]+presum[i][j-1]-presum[i-1][j-1];
            arr[i][j]+=presum[i][j];
        }
    }
    for(int i=1;i<=n;i++){
        for(int j=1;j<=n;j++){
            cout<<arr[i][j]<<" ";
        }
        cout<<endl;
    }
    return 0;
}

练习

P2280

https://www.luogu.com.cn/problem/P2280

注意,设置目标的时候必须统一差分图和前缀和图,也就是tar和presum,既然presum是要求从1开始,而题目给定的坐标是从0开始,所以必须tarx+1y+1+=v;

cpp 复制代码
#include<bits/stdc++.h>
using namespace std;

int n,m;
int presum[5005][5005];
int tar[5005][5005];
int main(){
    cin>>n>>m;
    for(int i=1;i<=n;i++){
        int x,y,v;
        cin>>x>>y>>v;
        tar[x+1][y+1]+=v;
    }
    for(int i=1;i<=5001;i++){
        for(int j=1;j<=5001;j++){
            presum[i][j]=presum[i][j-1]+presum[i-1][j]-presum[i-1][j-1]+tar[i][j];
        }
    }
    int max_val=0;
    m--;
    for(int i=1;i<=5001-m;i++){
        for(int j=1;j<=5001-m;j++){
            int a,b,c,d;
            a=i;b=j;c=i+m;d=j+m;
            int cur_val=presum[c][d]+presum[a-1][b-1]-presum[c][b-1]-presum[a-1][d];
            if(cur_val>max_val){
                max_val=cur_val;
            }
        }
    }
    cout<<max_val<<endl;
}

大综合 二分查找+前缀和+差分

https://www.luogu.com.cn/problem/P1083

P1083

核心是输出需要修改订单的申请人编号,也就是第几个m不满足,是一个单点搜索,要用二分。

m是单调的,可以使用

二分对象就是订单编号

搜索范围是0到M+1

推荐条件是 avai是否能满足。

接下来看check函数怎么写?

也就是查看在m个需求的时候是否能满足,

条件是ri>需求

需求可以用前缀和得到。

先差分,然后前缀和得到变化量,也就是每天的需求,然后再用条件判断。

一定要注意要初始化init,否则前缀和和差分会用此前的记录。

cpp 复制代码
#include<bits/stdc++.h>
using namespace std;
//nm在1e6 rd在1e9
long long presum[1000005];
int dst[1000005][3];
long long diff[1000005];
int ava[1000005];
int n,m;
int M;
void init(){
    memset(diff,0,sizeof(diff));
    memset(presum,0,sizeof(presum));
}
bool check(int M){
    //M次请求 先差分
    init();
    for(int i=0;i<M;i++){
        //从dst中取1 2 位
        diff[dst[i][1]]+=dst[i][0];
        diff[dst[i][2]+1]-=dst[i][0];
    }
    //然后前缀和
    for(int i=1;i<=n;i++){
        presum[i]=presum[i-1]+diff[i];
        if(presum[i]>ava[i]) return false;
    }
    return true;
}
int find(){
    long long L=0,R=m+1;
    while(R>L){
        int M=(L+R)/2;
        if(check(M)){//如果M个订单足以满足 应该增加订单的数量
            L=M+1;
        }else{
            R=M;
        }
    }
    return L;
}
int main(){
    cin>>n>>m;
    for(int i=1;i<=n;i++){
        cin>>ava[i];//存教室空位
    }
    for(int i=0;i<m;i++){
        cin>>dst[i][0]>>dst[i][1]>>dst[i][2];
    }
    //先单独测一下m个订单是否都能满足
    if(check(m)){
        cout<<0<<endl;
    }else{
        int result=find();
        cout<<-1<<endl<<result;
    }
    
    return 0;
}

P1314

注意是整数二分找到最接近s的W。

也就是找到刚好和s差不多的W。

然后测试左侧和右侧整数哪个更接近。

图的存储

无向图要比有向图的edge多开一倍!!!

邻接矩阵的实现(n<=5000可以用)

对于稀疏图和节点多的图就无法用了

邻接表

邻接表的实现------链式前向星

从head开始

下标代表每个顶点,内容代表下一个邻接节点的地址 ,edges的内容存一个set,包括节点号 和下一个邻接节点的地址。最后到0就是结束了

如何存边------头插法遍历顺序与插入相反

现在要存(1,5),找head的1,原先head1存的内容是指向下一个邻接节点的地址5,现在要存其指向值为5的edges,对应的地址是8,因此把8的内容改为(5,上一个节点的地址5),然后改head的内容为8

L是标记存到哪条边了。

先填充edges节点的值,包括邻接节点的值、边的权重,然后修改新节点的next指向旧节点fstu(原先head指向的第一个节点),然后把head指向自己

邻接表的遍历

邻接表的遍历是与添加顺序相反的。

设定一个初始p=fsti,只要p不为null,就一直往前找edgesp.next

二维vector存邻接表遍历顺序与插入相同

很明显的二维数组形式。

head作为行,后面的作为可变vector。

二维vector的逆序遍历
T419425

https://www.luogu.com.cn/problem/T419425

必须用for(auto it=rbegin();it!=rend();it++) cout<<*it<<" "; 不可以顺序输出。

cpp 复制代码
#include<iostream>
#include<vector>
using namespace std;

int N,E,Q;
vector<vector<int>> graph(100005);
int main(){
    cin>>N>>E>>Q;
    int n1,n2;
    //输入边
    for(int i=0;i<E;i++){
        cin>>n1>>n2;
        graph[n1].push_back(n2);
        graph[n2].push_back(n1);
    }
    int q;
    for(int i=0;i<Q;i++){
        cin>>q;
        
        for(auto it=graph[q].rbegin();it!=graph[q].rend();it++){
            cout<<*it<<" ";
        }
        cout<<endl;
    }
}

练习

T419392

https://www.luogu.com.cn/problem/T419392

cpp 复制代码
#include<bits/stdc++.h>
using namespace std;

//遍历本身就是逆序的
struct Edge{
    int v,next;
};

Edge edge[2000005];
int fst[100005];
int L=1;
void addEdge(int u,int v){
    edge[L].v=v;
    edge[L].next=fst[u];
    fst[u]=L;
    L++;
}

int main(){
    int N,M,Q;
    cin>>N>>M>>Q;
    int u,v;
    for(int i=0;i<M;i++){
        cin>>u>>v;
        addEdge(u,v);
        addEdge(v,u);
    }
    int q;
    for(int i=0;i<Q;i++){
        cin>>q;
        for(int p=fst[q];p;p=edge[p].next){
            cout<<edge[p].v<<" ";
        }
        cout<<endl;
    }
}

DFS

DFS的链式前向星实现

DFS的二维vector实现

cpp 复制代码
#include<bits/stdc++.h>
using namespace std;
vector<vector<int>> graph(1000005);
bool vis[1000005];
int N,M,u,v;
void dfs(int i){
    cout<<i<<" ";
    for(int j = graph[i].size() - 1; j >= 0; j--){//遍历他前驱节点
        if(!vis[graph[i][j]]){
            vis[graph[i][j]]=true;
            dfs(graph[i][j]);
        }
    }
}
int main(){
    
    cin>>N>>M;
    for(int i=0;i<M;i++){
        
        cin>>u>>v;
        graph[u].push_back(v);
    }
    for(int i=1;i<=N;i++){
        if(!vis[i]){
            vis[i]=true;
            dfs(i);
        }
    }
    cout<<endl;
    return 0;
}

最长路问题------带回溯的DFS

T429775

https://www.luogu.com.cn/problem/T429775

cpp 复制代码
#include<bits/stdc++.h>
using namespace std;
int N,M,u,v;
struct Edge{
    int v,next;
};
Edge E[20005];
int fst[1005];
int L=1;
bool vis[1005];
int cnt;//记录路长
int ans;
void addEdge(int u,int v){
    E[L].v=v;
    E[L].next=fst[u];
    fst[u]=L++;
}
void dfs(int i){
    ans=max(ans,cnt);
    for(int p=fst[i];p;p=E[p].next){
        int v=E[p].v;
        if(!vis[v]){
            vis[v]=true;
            cnt++;
            dfs(v);
            vis[v]=false;
            cnt--;
        }
    }
}
int main(){
    cin>>N>>M;
    for(int i=0;i<M;i++){
        cin>>u>>v;
        addEdge(u,v);
    }
    vis[1]=true; cnt++;
    dfs(1);
    cout<<ans;
}

统一模板

一定要明确搜索方式,是按行搜索?按个搜索?按节点搜索?

在这种搜索方式下,每个搜索节点会面临什么状态(选or不选(复习背包/取数游戏)?这一行的所有列(皇后))》?

dfs一开始就要考虑终止条件、切换遍历顺序条件(换行)

注意:如果涉及多次查询,记得memset图和vis数组。

如果是一次多个选项可以引入初始节点

确定是否需要回溯,如果要找多种可能,就一定要回溯。

确认是否需要vis,如果不允许两次选择同一个状态就vis

可以提前确认有哪些状态,for的时候就找这几种状态,然后if判断是否可以转移

按什么规律去找(按行?按层?)

这个规律下每次会遇到哪些状态?

如何判断哪些状态可行?

例题

P1706

https://www.luogu.com.cn/problem/P1706

cpp 复制代码
#include<iostream>
using namespace std;
bool vis[1005];
int n,cnt;
int ans[10];
void ptnans(){
    for(int i=0;i<n;i++){
        cout<<"    "<<ans[i];
    }
    cout<<endl;
}
void dfs(int i){
    if(cnt==n){
        ptnans();
    }
    for(int j=1;j<=n;j++){
        if(!vis[j]){
            vis[j]=true;
            ans[cnt++]=j;
            dfs(j);
            vis[j]=false;
            cnt--;
        }
    }
}
int main(){
    cin>>n;
    dfs(0);
}

二维地图DFS

https://www.luogu.com.cn/problem/P1605

P1605

cpp 复制代码
#include<bits/stdc++.h>
using namespace std;

int maze[10][10];
bool vis[10][10];
int N,M,T,cnt,FX,FY;
int curx,cury;
int f[4][2]={{0,1},{1,0},{0,-1},{-1,0}};
void dfs(int curx,int cury){
    if(curx==FX && cury==FY){
        cnt++;
        return;
    }
    for(int i=0;i<4;i++){
        //去看看当前四周
        int nextx=curx+f[i][0],nexty=cury+f[i][1];
        if(nextx<=N&&nextx>0 && nexty<=M&&nexty>0 && maze[nextx][nexty]!=-1&&!vis[nextx][nexty]){
            
            vis[nextx][nexty]=true;
            dfs(nextx,nexty);
            vis[nextx][nexty]=false;
            
        }
    }
}
int main(){
    cin>>N>>M>>T;
    cin>>curx>>cury>>FX>>FY;
    for(int i=0;i<T;i++){
        int obsx,obsy;
        cin>>obsx>>obsy;
        maze[obsx][obsy]=-1;
    }
    vis[curx][cury]=true;
    dfs(curx,cury);
    cout<<cnt<<endl;
    
}

对角线规则

对角线的值和是定值;反对角线的行列差是定值(由于存在负数必须偏移)。

皇后问题 P1219

https://www.luogu.com.cn/problem/P1219

按什么规律去找(按行?按层?)

这个规律下每次会遇到哪些状态?

如何判断哪些状态可行?

按行遍历,每一列都是状态,需要判断是否在此前任意节点的对角线、反对角线、列上。

如何存储这些:用vis数组,第一行存列占用,第二行存对角线占用,第三行存储反对角线占用。

cpp 复制代码
if(!vis[0][j]&&!vis[1][i+j]&&!vis[2][i-j+20])
cpp 复制代码
#include<bits/stdc++.h>
using namespace std;
int vis[3][50];
int ans[15];
int n,cnt,ansnum;
void dfs(int i){
    if(i==n){
        ansnum++;
        if(ansnum<=3){//输出ans
            for(int i=0;i<n;i++){
                cout<<ans[i]<<" ";
            }
            cout<<endl;
        }
    }
    for(int j=1;j<=n;j++){
        //用vis数组,第一行存列占用,第二行存对角线占用,第三行存储反对角线占用
        if(!vis[0][j]&&!vis[1][i+j]&&!vis[2][i-j+20]){
            vis[0][j]=vis[1][i+j]=vis[2][i-j+20]=true;
            ans[cnt++]=j;
            dfs(i+1);
            vis[0][j]=vis[1][i+j]=vis[2][i-j+20]=false;
            cnt--;
        }
    }
}
int main(){
    cin>>n;
    dfs(0);
    cout<<ansnum<<endl;
}

复习背包/DFS P2392

https://www.luogu.com.cn/problem/P2392

很诡异的解法,

按什么规律去找(按行?按层?)

这个规律下每次会遇到哪些状态?

如何判断哪些状态可行?

按题目去找到,每道题目两种情况,一种是选,一种不选,选不选都会进入dfs(k,i+1)状态

都是可以的,不需要判断可行性。

cpp 复制代码
#include<bits/stdc++.h>
using namespace std;

int s[4];//科目题目数量
int t[4][100];
int ans[4],cur[4],half[4];

void dfs(int k,int i){//第k门 第i题
    if(cur[k]>=half[k]){//如果超过一半时间就停止,然后对比
        ans[k]=min(ans[k],cur[k]);
        return;
    }
    if(i==s[k]) return ;//或者直接达到最后一道题
    //否则就尝试把这道题加入cur
    cur[k]+=t[k][i];
    dfs(k,i+1);
    cur[k]-=t[k][i];//另一种情况是把i题放到另一侧,然后继续dfs
    dfs(k,i+1);
    
}
int res=0;
int main(){
    cin>>s[0]>>s[1]>>s[2]>>s[3];
    for(int i=0;i<4;i++){
        for(int j=0;j<s[i];j++){
            cin>>t[i][j];
            ans[i]+=t[i][j];
        }
        half[i]=(ans[i]+1)/2;
        dfs(i,0);
        res+=ans[i];
    }
    cout<<res<<endl;
}

细胞数量 P1451

遇到需要扩展一片的题,先考虑DFS,对图的所有位置进行一次DFS

https://www.luogu.com.cn/problem/P1451

这道题告诉我们,不一定是所有DFS都需要内部判断的,有时候只需要打标记即可

cpp 复制代码
//通过DFS搜索一片的细胞 给这一片都打上vis
#include<bits/stdc++.h>
using namespace std;
int n,m,cnt;
int maze[105][105];
int vis[105][105];
int f[4][2]={{0,1},{1,0},{0,-1},{-1,0}};
int curx,cury,nextx,nexty;
void dfs(int curx,int cury){
    for(int i=0;i<4;i++){
        nextx=curx+f[i][0];nexty=cury+f[i][1];
        if(nextx&&nexty&&nextx<=n&&nexty<=m&&!vis[nextx][nexty]&&maze[nextx][nexty]!=0){
            vis[nextx][nexty]=true;
            dfs(nextx,nexty);
            
        }
    }
}

int main(){
    cin>>n>>m;
    char temp;
    for(int i=1;i<=n;i++){
        for(int j=1;j<=m;j++){
            cin>>temp;
            maze[i][j]=temp-'0';
        }
    }
    for(int i=1;i<=n;i++){
        for(int j=1;j<=m;j++){
            if(!vis[i][j]&&maze[i][j]){
                cnt++;
                vis[i][j]=true;
                dfs(i,j);
                
            }
        }
    }
    cout<<cnt<<endl;
}

vis的双重锁问题 取数游戏 P1123

https://www.luogu.com.cn/problem/P1123

这道题的难点在于不是相邻扩展的,而是只能向右下拓展的,这类题目最难就在于找到拓展方式,判定函数都好写。

下面的代码是WA的,因为vis被设为了bool,只有01状态。但是有可能ABA这种情况,B被锁了两次,此时撤回A就不可以直接设B=false了。这时就需要引入数值锁。

cpp 复制代码
//这道题的难点在于不是相邻扩展的,而是只能向右下拓展的,这类题目最难就在于找到拓展方式,判定函数都好写。
//第二个坑:由于可能vis重合,有双重锁,不能一次就改为false。
#include<bits/stdc++.h>
using namespace std;
int vis[7][7];
int maze[7][7];
int T;
int N,M;
int sum,ans;
int f[8][2]={{-1,-1},{-1,0},{-1,1},{0,-1},{0,1},{1,-1},{1,0},{1,1}};
void save(int i,int j){//把当前周伟8格vis
    for(int k=0;k<8;k++){
        int nx = i + f[k][0];
        int ny = j + f[k][1];
        if(nx >= 1 && nx <= N && ny >= 1 && ny <= M) {
            //vis[nx][ny]==true;
            vis[nx][ny]++;
        }
    }
    
    sum+=maze[i][j];
}
void restore(int i,int j){//把当前周伟8格vis
    for(int k=0;k<8;k++){
        int nx = i + f[k][0];
        int ny = j + f[k][1];
        if(nx >= 1 && nx <= N && ny >= 1 && ny <= M) {
            //vis[nx][ny]=false;
            vis[nx][ny]--;
        }
    }
    sum-=maze[i][j];
}



void dfs(int i,int j){
    if(i==N+1){
        ans=max(ans,sum);
        return;
    }
    if(j==M+1){
        dfs(i+1,1);
        return;
    }
    //每个数作为一个节点,每次都有取和不取两种状态
    //不取
    dfs(i,j+1);

    //取
    if(!vis[i][j]){
        save(i,j);
        dfs(i,j+1);
        restore(i,j);
    }
    
    
}
int main(){
    cin>>T;
    while(T--){
        ans=0;
        sum=0;
        cin>>N>>M;
        memset(maze,0,sizeof(maze));
        memset(vis,0,sizeof(vis));
        for(int i=1;i<=N;i++){
            for(int j=1;j<=M;j++){
                cin>>maze[i][j];
            }
        }
        sum=0;
        dfs(1,1);
        cout<<ans<<endl;
    } 
}
    

BFS(最短路问题)

BFS的链式前向星实现

这里遍历邻接节点的目的不是去搜索,而是放到队列中,只搜索队列最前面的

模板 BFS搜索最短路问题

注意!!stp一定要加f前缀!!!!!!!!!

不要被条件约束住,查询问题可以搜完再查,没必要搜的时候查!!!

多次查询一定要清空vis啊!!!

用swap清空queue!

cpp 复制代码
queue<Node> q;
memset(vis,0,sizeof(vis));
memset(maze,-1,sizeof(maze));
queue<Node> empty_q; 
swap(q,empty_q);

注意,超过2000*2000的数组不可以memset初始化,不然会TLE!

多次查询这种题只需要初始化vis即可,每次查询不一致即可。maze会被覆盖,pre_x\pre_y\pre_dir也会被覆盖。

这是一个极好的模板,Node用于记录关键信息,这道题用的是长度,下一道题用是坐标,Node的内容就是x和y。

注意!! 由于使用了Node结构体存储u和长度len。

因此后续访问u都有用f。

cpp 复制代码
#include<iostream>
#include<queue>
using namespace std;
bool vis[100005]={false};
int N,M;
int fst[10005]={0};
struct Edge{
    int v,next;
};
struct Node{
    int u,len;
    Node(int u1=1,int length=0){
        u=u1;len=length;
    }
};
Edge E[200005];
int L=1;
void addEdge(int u,int v){
    E[L].v=v;
    E[L].next=fst[u];
    fst[u]=L++;
}
queue<Node> q;
int main(){
    cin>>N>>M;
    for(int i=0;i<M;i++){
        int u,v;
        cin>>u>>v;
        addEdge(u,v);
        addEdge(v,u);
    }
    q.push(Node(1,0));
    vis[1]=true;
    while(!q.empty()){
        Node f=q.front();
        //cout<<u<<" ";
        if(f.u==N){
            cout<<f.len<<endl;
            return 0;
        }
        q.pop();
        for(int p=fst[f.u];p;p=E[p].next){
            int v=E[p].v;
            if(!vis[v]){
                vis[v]=true;
                q.push(Node(v,f.len+1));
            }
        }
    }
}

BFS 二维迷宫 B3625(提前截止)

https://www.luogu.com.cn/problem/B3625

cpp 复制代码
#include<bits/stdc++.h>
using namespace std;
bool vis[105][105];
int maze[105][105];
int h[4][2]={{-1,0},{0,-1},{1,0},{0,1}};
int N,M;

struct Node{
    int x,y;
    Node(int x1=1,int y1=1){
        x=x1;y=y1;
    }
};
queue<Node> q;
int main(){
    cin>>N>>M;
    char temp;
    for(int i=1;i<=N;i++){
        for(int j=1;j<=M;j++){
            cin>>temp;
            if(temp=='#'){
                maze[i][j]=0;
            }else{
                maze[i][j]=1;
            }
        }
    }
    q.push(Node(1,1));
    vis[1][1]=true;
    while(!q.empty()){
        Node f=q.front();
        if(f.x==N && f.y==M){
            cout<<"Yes"<<endl;
            return 0;
        }
        q.pop();
        //接下来要探索这个迷宫
        //上下左右+约束
        int nx,ny;
        for(int i=0;i<4;i++){
            nx=f.x+h[i][0];ny=f.y+h[i][1];
            if(nx&&ny&&nx<=N&&ny<=M&&!vis[nx][ny]&&maze[nx][ny]){
                vis[nx][ny]=true;
                q.push(Node(nx,ny));
            }
        }
    }
    cout<<"No"<<endl;
}

BFS 二维迷宫 P1443(非提前截止)

https://www.luogu.com.cn/problem/P1443

这道题要求遍历所有情况,因此不添加提前截止代码。

cpp 复制代码
#include<bits/stdc++.h>
using namespace std;
int N,M,x,y;
int h[8][2]={{-1,2},{1,2},{-2,1},{2,1},{-2,-1},{-1,-2},{1,-2},{2,-1}};
int vis[405][405];
int maze[405][405];
void ptnmaze(){
    for(int i=1;i<=N;i++){
        for(int j=1;j<=M;j++){
            cout<<maze[i][j]<<" ";
        }
        cout<<endl;
    }
}
//最少步数-》最短路问题
struct Node{
    int x,y,stp;
    Node(int x1=0,int y1=0,int stp1=0){
        x=x1;y=y1;stp=stp1;
    }
};
queue<Node> q;
int main(){
    cin>>N>>M>>x>>y;
    memset(maze,-1,sizeof(maze));
    q.push(Node(x,y,0));
    vis[x][y]=true;
    maze[x][y]=0;
    while(!q.empty()){
        Node f=q.front();
        //终止条件是什么
        // if(f.stp>=N*M){
        //     //一个过限条件
        //     ptnmaze();
        //     return 0;
        // }
        q.pop();
        int nx,ny;
        for(int i=0;i<8;i++){
            nx=f.x+h[i][0];ny=f.y+h[i][1];
            if(nx&&ny&&nx<=N&&ny<=M&&!vis[nx][ny]){
                vis[nx][ny]=true;
                maze[nx][ny]=f.stp+1;
                q.push(Node(nx,ny,f.stp+1));
            }
        }
    }
    for(int i=1;i<=N;i++){
        for(int j=1;j<=M;j++){
            cout<<maze[i][j]<<" ";
        }
        cout<<endl;
    }
}

练习1 T343500 细胞问题

https://www.luogu.com.cn/problem/T434500

我犯了2个错误。

如果从1开始存,那么探索通过条件应该是if(nx<=N && ny<=M)

只要当前没有vis且maze为0就说明发现了新细胞。

cpp 复制代码
#include<bits/stdc++.h>
using namespace std;
int N,M;
int maze[105][105];
int vis[105][105];
int h[4][2]={{-1,0},{0,-1},{1,0},{0,1}};
//对所有点开广搜 每个点都看看四周 如果有菲林就标记 没有截止条件
struct Node{
    int x,y;
    Node(int x1=0,int y1=0){
        x=x1;y=y1;
    }
};
queue<Node> q;

int main(){
    char temp;
    cin>>N>>M;
    for(int i=1;i<=N;i++){
        for(int j=1;j<=M;j++){
            cin>>temp;
            maze[i][j]=temp-'0';
        }
    }
    int ans=0;
    memset(vis,0,sizeof(vis));
    int flag=0;
    for(int i=1;i<=N;i++){
        for(int j=1;j<=M;j++){
            
            if(maze[i][j]&&!vis[i][j]){
                ans++;
                q.push(Node(i,j));
                vis[i][j]=true;
                
                while(!q.empty()){
                    Node f=q.front();
                    q.pop();
                    int nx,ny;
                    for(int k=0;k<4;k++){
                        nx=f.x+h[k][0];ny=f.y+h[k][1];
                        if(nx&&ny&&nx<=N&&ny<=M&&!vis[nx][ny]&&maze[nx][ny]){
                            vis[nx][ny]=true;
                            flag=1;
                            q.push(Node(nx,ny));
                            
                        }
                    }
                }
            }
            
        }
    }
    cout<<ans<<endl;
}

练习2 P1746 离开中山路

https://www.luogu.com.cn/problem/P1746

我犯的错:记录最短路要用Node内变量,不能写外面。

cpp 复制代码
#include<bits/stdc++.h>
using namespace std;
//提前截止的BFS最短路
int maze[1005][1005];
bool vis[1005][1005];
int h[4][2]={{-1,0},{0,-1},{1,0},{0,1}};
int n;

struct Node{
    int x,y,stp;
    Node(int x1=0,int y1=0,int stp1=0){
        x=x1;y=y1;stp=stp1;
    }
};
queue<Node> q;

int main(){
    int x1,y1,x2,y2;
    cin>>n;
    string s;
    for(int i=1;i<=n;i++){
        cin>>s;
        char temp;
        //int p;
        //cin>>p;
        for(int j=1;j<=n;j++){
            //int temp=p%(10^j);
            temp=s[j-1];
            maze[i][j]=temp-'0';
        }
    }
    cin>>x1>>y1>>x2>>y2;
    q.push(Node(x1,y1,0));
    vis[x1][y1]=true;
    while(!q.empty()){
        Node f=q.front();
        
        if(f.x==x2&&f.y==y2){
            cout<<f.stp<<endl;
            return 0;
        }
        q.pop();
        int nx,ny;
        for(int i=0;i<4;i++){
            nx=f.x+h[i][0];ny=f.y+h[i][1];
            if(nx&&ny&&nx<=n&&ny<=n&&!vis[nx][ny]&&!maze[nx][ny]){
                vis[nx][ny]=true;
                q.push(Node(nx,ny,f.stp+1));
            }
        }
    }
    cout<<-1<<endl;
}

多源DFS(一次塞几个Node进q) 练习3 血色先锋队 P1332

https://www.luogu.com.cn/problem/P1332

错误示范:

初次思路是先记下来所有领袖的位置,然后搜索到的时候记录下来。

但是这样复杂度太高了,而且要额外的映射去记录输入和最短的关系,而b的数量级很大,不宜用二维数组,就死了。

又忘了f.stp 我是傻逼

这道题正解是全部搜索完,得到每个点的最短值,然后拿这张搜索完的图去查找!

cpp 复制代码
//多源的扩散 多源的BFS 就是直接往q中塞好几个源
#include<bits/stdc++.h>
using namespace std;
int maze[505][505];
bool vis[505][505];
int h[4][2]={{-1,0},{0,-1},{1,0},{0,1}};
struct Node{
    int x,y,stp;
    Node(int x1=0,int y1=0,int stp1=0){
        x=x1;y=y1;stp=stp1;
    }
};
queue<Node> q;
int n,m,a,b;
int main(){
    cin>>n>>m>>a>>b;
    for(int i=0;i<a;i++){
        int x,y;
        cin>>x>>y;
        q.push(Node(x,y,0));
        vis[x][y]=true;
        maze[x][y]=0;
    }
    
    while(!q.empty()){
        Node f=q.front();
        maze[f.x][f.y]=f.stp;
        q.pop();
        int nx,ny;
        for(int i=0;i<4;i++){
            nx=f.x+h[i][0];ny=f.y+h[i][1];
            if(nx&&ny&&nx<=n&&ny<=m&&!vis[nx][ny]){
                vis[nx][ny]=true;
                q.push(Node(nx,ny,f.stp+1));
            }
        }
    }
    for(int i=0;i<b;i++){
        int x,y;
        cin>>x>>y;
        cout<<maze[x][y]<<endl;
    }
    
}

带时间的DFS P3395 用swap清空queue

重点在于如何情况queue做初始化

cpp 复制代码
queue<Node> q;
memset(vis,0,sizeof(vis));
memset(maze,-1,sizeof(maze));
queue<Node> empty_q; 
swap(q,empty_q);

https://www.luogu.com.cn/problem/P3395

cpp 复制代码
#include<bits/stdc++.h>
using namespace std;
//探索一个下一层节点时会修改一次maze而已
//引入新的维度,把maze做成3维的,第三维是时间,一开始设置所有的第三维为-1,然后每次有输入就拉进去,然后if检查的时候要么nxny的第三维为-1,要么当前的时间点,也就是f.stp<=第三维
int T;
int n;
int maze[1005][1005];
int vis[1005][1005];
int h[4][2]={{-1,0},{0,-1},{1,0},{0,1}};
struct Node{
    int x,y,t;
    Node(int x1=0,int y1=0,int t1=-1){
        x=x1;y=y1;t=t1;
    }
};
queue<Node> q;


int main(){
    cin>>T;
    while(T--){
        memset(vis,0,sizeof(vis));
        memset(maze,-1,sizeof(maze));
        queue<Node> empty_q; 
        swap(q, empty_q); 
        cin>>n;
        int flag=0;
        for(int i=1;i<=2*n-2;i++){
            int x,y;
            cin>>x>>y;
            maze[x][y]=i;
        }
        
        
        q.push(Node(1,1,0));
        vis[1][1]=true;
        while(!q.empty()){
            Node f=q.front();
            if(f.x==n&&f.y==n){
                flag=1;
                break;
            }
            q.pop();
            int nx,ny;
            for(int i=0;i<4;i++){
                nx=f.x+h[i][0];ny=f.y+h[i][1];
                if(nx&&ny&&nx<=n&&ny<=n&&!vis[nx][ny]&&(f.t<=maze[nx][ny] || maze[nx][ny]==-1)){
                    vis[nx][ny]=true;
                    q.push(Node(nx,ny,f.t+1));
                }
            }
        }
        if(flag){
            cout<<"Yes"<<endl;
        }else{
            cout<<"No"<<endl;
        }
        
    }
}

需要输出路径的DFS P10234

https://www.luogu.com.cn/problem/P10234

必须开三个数组,存该点的前一个x,该点的前一个y,该点的前一个点到当前的方向。都是二维向一位的映射。

注意,超过2000*2000的数组不可以memset初始化,不然会TLE!

多次查询这种题只需要初始化vis即可,每次查询不一致即可。maze会被覆盖,pre_x\pre_y\pre_dir也会被覆盖。

cpp 复制代码
// pre_x/y 记录前驱节点的坐标,pre_dir 记录从前驱走到当前的方向
int pre_x[2005][2005], pre_y[2005][2005];
char pre_dir[2005][2005];

char dir_char[4] = {'U', 'D', 'L', 'R'};

pre_x[nx][ny] = f.x; // 记下:我是从 f.x 走过来的
pre_y[nx][ny] = f.y; // 记下:我是从 f.y 走过来的
pre_dir[nx][ny] = dir_char[i];

路径递归输出模板

cpp 复制代码
            string path="";
            int cx=n,cy=m;//从终点开始
            while(pre_x[cx][cy]!=-1){//如果cxcy这个点的前驱不是-1的话就往前遍历
                path+=pre_dir[cx][cy];//将上一步步骤存进path
                int tx=pre_x[cx][cy];
                int ty=pre_y[cx][cy];
                cx=tx;//向前迭代
                cy=ty;
            }
            //由于是向前迭代,因此要reverse一下字符串
            reverse(path.begin(),path.end());
            cout<<path<<endl;
cpp 复制代码
#include<bits/stdc++.h>
using namespace std;

int maze[2005][2005];
int vis[2005][2005];
int n,m,T;
int h[4][2]={{-1,0},{1,0},{0,-1},{0,1}};
int pre_x[2005][2005],pre_y[2005][2005];
int pre_dir[2005][2005];
char dir_char[4]={'U','D','L','R'};
struct Node{
    int x,y,stp;
    Node(int x1=0,int y1=0,int stp1=0){
        x=x1;y=y1;stp=stp1;
    }
};
queue<Node> q;


int main(){
    cin>>T;
    while(T--){
        memset(vis,0,sizeof(vis));

        queue<Node> empty;
        swap(q,empty);

        
        cin>>n>>m;
        int flag=0;
        int min_len=0;
        
        char temp;
        for(int i=1;i<=n;i++){
            for(int j=1;j<=m;j++){
                cin>>temp;
                maze[i][j]=temp-'0';
            }
        }
        q.push(Node(1,1,0));
        vis[1][1]=true;
        pre_x[1][1]=-1;pre_y[1][1]=-1;
        while(!q.empty()){
            Node f=q.front();
            if(f.x==n&&f.y==m){//如果找到终点
                flag=1;
                min_len=f.stp;
                break;
            }
            q.pop();
            int nx,ny;
            for(int i=0;i<4;i++){
                nx=f.x+h[i][0];ny=f.y+h[i][1];
                if(nx&&ny&&nx<=n&&ny<=m&&!vis[nx][ny]&&maze[f.x][f.y]!=maze[nx][ny]){
                    vis[nx][ny]=true;
                    pre_x[nx][ny]=f.x;
                    pre_y[nx][ny]=f.y;
                    pre_dir[nx][ny]=dir_char[i];
                    q.push(Node(nx,ny,f.stp+1));
                }
            }
        }
        if(flag){
            cout<<min_len<<endl;
            string path="";
            int cx=n,cy=m;//从终点开始
            while(pre_x[cx][cy]!=-1){//如果cxcy这个点的前驱不是-1的话就往前遍历
                path+=pre_dir[cx][cy];//将上一步步骤存进path
                int tx=pre_x[cx][cy];
                int ty=pre_y[cx][cy];
                cx=tx;//向前迭代
                cy=ty;
            }
            //由于是向前迭代,因此要reverse一下字符串
            reverse(path.begin(),path.end());
            cout<<path<<endl;
            
        }else{
            cout<<-1<<endl;
        }
        
        
        
    }
    
}

我是傻逼的解法,解不了

cpp 复制代码
#include<bits/stdc++.h>
using namespace std;
//带递归路径的BFS 在Node内置一个记录上一个点的pre即可。

//用一个char类型的deque记录UDLR,然后直接倒着取出来
int maze[2005][2005];
int vis[2005][2005];
int n,m,T;
int h[4][2]={{-1,0},{1,0},{0,-1},{0,1}};
const map<int,char> ma={{0,'U'},{1,'D'},{2,'L'},{3,'R'}};
deque<char> dc;
struct Node{
    int x,y,stp;
    Node(int x1=0,int y1=0,int stp1=0){
        x=x1;y=y1;stp=stp1;
    }
};
queue<Node> q;
void ptnvers(){
    for(auto it=dc.rbegin();it!=dc.rend();it++){
        cout<<*it<<endl;
    }
    return;
}

int main(){
    cin>>T;
    while(T--){
        memset(vis,0,sizeof(vis));
        memset(maze,-1,sizeof(maze));
        dc.clear();
        queue<Node> empty;
        swap(q,empty);
        cin>>n>>m;
        int flag=0;
        int min_len=INT_MAX;
        
        char temp;
        for(int i=1;i<=n;i++){
            for(int j=1;j<=m;j++){
                cin>>temp;
                maze[i][j]=temp-'0';
            }
        }
        q.push(Node(1,1,0));
        vis[1][1]=true;
        while(!q.empty()){
            Node f=q.front();
            if(f.x==n&&f.y==m){//如果找到终点
                flag=1;
                min_len=f.stp;
                break;
            }
            int nx,ny;
            for(int i=0;i<4;i++){
                nx=f.x+h[i][0];ny=f.y+h[i][1];
                if(nx&&ny&&nx<=n&&ny<=m&&!vis[nx][ny]&&maze[f.x][f.y]!=maze[nx][ny]){
                    vis[nx][ny]=true;
                    auto it=ma.find(i);
                    dc.push_back(it->first);
                    q.push(Node(nx,ny,f.stp+1));
                }
            }
        }
        if(flag){
            cout<<min_len<<endl;
            ptnvers();
        }else{
            cout<<-1<<endl;
        }
        
        
        
    }
    
}

DFS VS BFS

树的存储

孩子表示法

双亲表示法

孩子兄弟表示法

二叉树遍历

模板 B3642 给定左右节点的二叉树构建

https://www.luogu.com.cn/problem/B3642

将根节点存在下标的位置,然后左右子节点就是RN和LN。

然后根节点的下标是1,那么其两个子节点就是BT1.RN,BT1.LN。

输入的时候只需要根据根节点的下标输入即可。

cpp 复制代码
#include<bits/stdc++.h>
using namespace std;

struct Node{
    int LN,RN;
};
Node BT[1000005];
int n;

void preOrder(int i){
    //根左右
    cout<<i<<" ";
    //如果有左子树
    if(BT[i].LN) preOrder(BT[i].LN);
    if(BT[i].RN) preOrder(BT[i].RN);
}

void inOrder(int i){
    //左根右
    //如果有左子树
    if(BT[i].LN) inOrder(BT[i].LN);
    cout<<i<<" ";
    if(BT[i].RN) inOrder(BT[i].RN);
}

void postOrder(int i){
    //左右根
    //如果有左子树
    if(BT[i].LN) postOrder(BT[i].LN);
    if(BT[i].RN) postOrder(BT[i].RN);
    cout<<i<<" ";
}

int main(){
    cin>>n;
    for(int i=1;i<=n;i++){
        cin>>BT[i].LN>>BT[i].RN;
    }
    preOrder(1);
    cout<<endl;
    inOrder(1);
    cout<<endl;
    postOrder(1);
    cout<<endl;
    
    
}

二叉树的恢复

前+中 中+后都是可以的。

模板 P1030

https://www.luogu.com.cn/problem/P1030

输出的第一个一定是根,而能确定根的无非是前后序。

cpp 复制代码
#include<bits/stdc++.h>
using namespace std;
string inOrder,postOrder;
void findPreOrder(string in,string post){
    if(in.empty()) return;//如果string排完了就返回
    char root=post[post.size()-1];//取根 根一定来自前序后序
    cout<<root;//每次都输出自己,自己一定是子树的根 【根】
    //按照根切割子树,在前后序找到左右子树然后递归
    int t=in.find(root);//string是容器,可以用find
    findPreOrder(in.substr(0,t),post.substr(0,t));//【左】
    findPreOrder(in.substr(t+1),post.substr(t,post.size()-t-1));//【右】
}

void findPostOrder(string in,string pre){
    if(in.empty()) return ;
    char root=pre[0];
    int t=in.find(root);
    findPostOrder(in.substr(0,t),pre.substr(1,t));//【左】
    findPostOrder(in.substr(t+1),pre.substr(t+1));//【右】
    cout<<root;//【根】
}
int main(){
    cin>>inOrder>>postOrder;
    findPreOrder(inOrder,postOrder);
}

前序+后序的不唯一性 P1229

https://www.luogu.com.cn/problem/P1229

为什么前序+后序无法确定唯一的中序?

只有当二叉树中存在 "只有一个孩子的节点" 时,不确定性才会产生。

  • 如果一个节点有两个孩子(左、右),那么在前序和后序中,这两个孩子的相对位置和父子关系是确定的,无法互换。
  • 如果一个节点 只有一个孩子 ,我们无法仅凭前序和后序判断这个孩子是 左孩子 还是 右孩子

就是左右根和根左右,如果左右只有一个人,你不知道这个人是左还是右。

DCE CED这种情况可以唯一恢复。即要求包夹的部分是顺序。

而 GIH HIG则不可以,H包夹的部分是逆序就会出现不知道左右子树哪边为空的情况。

那么判定条件就是看前后序相邻的逆序对了。

先把s1(先序)反转,然后一个一个扫描长度为二的子串,如果在s2(后序)也有,那么就*2.

cpp 复制代码
#include<bits/stdc++.h>
using namespace std;

string s1,s2;
long long ans=1;
int main(){
    cin>>s1>>s2;
    reverse(s1.begin(),s1.end());
    for(int i=0;i<s1.size()-1;i++){
        if(int(s2.find(s1.substr(i,2)))!=-1){
            ans*=2;
        }
    }
    cout<<ans;
}

练习1 二叉树深度 P4913

https://www.luogu.com.cn/problem/P4913

cpp 复制代码
#include<bits/stdc++.h>
using namespace std;
struct Node{
    int RN,LN;
};
Node BT[1000005];
int n;
int Depth;
void dfs(int i,int d){
    if(i==0) return;//叶子节点
    Depth=max(Depth,d);
    dfs(BT[i].LN,d+1);
    dfs(BT[i].RN,d+1);
}

int main(){
    cin>>n;
    for(int i=1;i<=n;i++){
        cin>>BT[i].LN>>BT[i].RN;
    }
    dfs(1,1);
    cout<<Depth;
    
    
}

二叉搜索树

二叉搜索树可以通过中序遍历:左根右 的次序完成从小到大的遍历

二叉搜索树的构建 递归建树

递归建树模板 P2171

https://www.luogu.com.cn/problem/P2171

cpp 复制代码
int buildBST(int cur,int i,int depth){
    if(cur==0){//如果当前节点是空的就放这里
        Depth=max(depth,Depth);//记录最大树高
        return i;
    }
    //否则 如果插入节点的值比当前节点的值大,向右递归
    if(BST[i].val>BST[cur].val){
        BST[cur].RN=buildBST(BST[cur].RN,i,depth+1);
        //这里用BST[cur].RN接受是因为最后一层时,插入后必须连接上他的父节点才行。
        //depth是继承上一个节点的,没有问题。
    }else{
        BST[cur].LN=buildBST(BST[cur].LN,i,depth+1);
    }
    return cur;
}
cpp 复制代码
#include<bits/stdc++.h>
using namespace std;

struct Node{
    int LN,RN;
    int val;
};
int n;
Node BST[300005];
int Depth;

int buildBST(int cur,int i,int depth){
    if(cur==0){//如果当前节点是空的就放这里
        Depth=max(depth,Depth);//记录最大树高
        return i;
    }
    //否则 如果插入节点的值比当前节点的值大,向右递归
    if(BST[i].val>BST[cur].val){
        BST[cur].RN=buildBST(BST[cur].RN,i,depth+1);
        //这里用BST[cur].RN接受是因为最后一层时,插入后必须连接上他的父节点才行。
        //depth是继承上一个节点的,没有问题。
    }else{
        BST[cur].LN=buildBST(BST[cur].LN,i,depth+1);
    }
    return cur;
}

void postOrder(int i){
    if(i==0){
        return;
    }
    postOrder(BST[i].LN);
    postOrder(BST[i].RN);
    cout<<BST[i].val<<endl;
}

int main(){
    cin>>n;
    int val;
    cin>>BST[1].val;
    Depth=1;
    for(int i=2;i<=n;i++){
        cin>>BST[i].val;
        buildBST(1,i,1);//递归建树
    }
    cout<<"deep="<<Depth<<endl;
    postOrder(1);
}

贪心------只适用于单一的 可切割 的部分背包问题

练习 P2240

https://www.luogu.com.cn/problem/P2240

cpp 复制代码
#include<bits/stdc++.h>
using namespace std;
int N,T;
struct Gold{
    int m,v;
    double p;
};


bool cmp(Gold &a,Gold &b){
    return a.p>b.p;
}
int main(){
    cin>>N>>T;
    vector<Gold> G(N);
    for(int i=0;i<N;i++){
        cin>>G[i].m>>G[i].v;
        G[i].p=(double)G[i].v/G[i].m;
    }
    sort(G.begin(),G.end(),cmp);
    double curm=0;
    double curv=0;
    for(int i=0;i<N;i++){
        if(G[i].m+curm<=T){//如果全装得下
            curv+=G[i].v;
            curm+=G[i].m;
        }else{//如果只装得下一部分
            curv+=(T-curm)*G[i].p;
            break;
        }
    }
    cout<<fixed<<setprecision(2)<<curv<<endl;
}

练习 排队打水 P1223 【第二关键词排序】

https://www.luogu.com.cn/problem/P1223

cpp 复制代码
using namespace std;
struct Node {
    int idx, t;
};
bool cmp(Node& a, Node& b) {
    return a.t < b.t;//输出时间小的优先
}
int n;
int main() {
    //SJF模型
    //
    cin >> n;
    vector<Node> P(n);
    for (int i = 0; i < n; i++) {
        cin >> P[i].t; P[i].idx = i;
    }
    sort(P.begin(), P.end(), cmp);
    double T = 0;
    double cur_t = 0;
    for (int i = 0; i < n; i++) {
        cout << P[i].idx+1 << " ";
        if (i >= 1) {
            cur_t += P[i - 1].t;
            T += cur_t;
            //cout << "T is:" << T << endl;
        }
    }
    cout << endl;
    cout << fixed << setprecision(2) << (double)T / n << endl;;
}

DP动态规划

需不需要从头算?

需要就是DFS BFS 不需要就是DP,需要全部就是BFS

一定要走一下前几个,看看方程对不对,否则白干

状态转移方程多种多样,比如爬楼梯,Fi就有2种可能。选硬币则是有多个可能进入。
最长子序列则是有条件的多个可能转移进来。

有条件的转移。

DP能解决所有的贪心问题

递归 自顶向下分解 n从大到小

递归的效率很低,有很多重复计算

使用备忘录记录,下次搜索发现如果前面有计算过就不算了

递推 自底向上合成 n从小到大

这样就可以直接在数组上实现了,因为后一个的数值都可以直接从记录中找到,无需额外递归计算

核心在于如何定义解?(状态)

如何找到状态之间的递推关系(方程)

模板 P1216

https://www.luogu.com.cn/problem/P1216

cpp 复制代码
#include<bits/stdc++.h>
using namespace std;

//最大问题:求路径的最大值-》子问题:到每个点的路径最大值
//状态定义:到每个点的最大值
//初始状态输入决定
//用二维数组表示
//转移方程:当前节点的最大值=max(前两个点)+当前值
//F[i][j]=max(F[i-1][j-1],F[i-1][j])+a[j];

int F[1005][1005];
int r;
int main(){
    cin>>r;
    F[0][0]=F[0][1]=0;
    for(int i=1;i<=r;i++){
        for(int j=1;j<=i;j++){
            int t;
            cin>>t;
            F[i][j]=max(F[i-1][j],F[i-1][j-1])+t;
        }
    }
    int ans=0;
    for(int i=1;i<=r;i++){
        ans=max(ans,F[r][i]);
    }
    cout<<ans<<endl;
}

空间优化------滚动数组

从后向前覆盖

cpp 复制代码
    for(int i=1;i<=r;i++){
        for(int j=i;j>=1;j--){
            int t;
            cin>>t;
            F[j]=max(F[j],F[j-1])+t;
        }
    }

cpp 复制代码
#include<bits/stdc++.h>
using namespace std;
int F[1005];
int r;
int main(){
    cin>>r;
    F[0]=F[1]=0;
    for(int i=1;i<=r;i++){
        for(int j=i;j>=1;j--){
            int t;
            cin>>t;
            F[j]=max(F[j],F[j-1])+t;
        }
    }
    int ans=0;
    for(int i=1;i<=r;i++){
        ans=max(ans,F[i]);
    }
    cout<<ans<<endl;
}

练习 取硬币 P2840

取模:出现取模了那肯定方案特别多

加法取模是同态的,可以在过程中取也可以在末尾取,但是最好在过程中取,防溢出。

https://www.luogu.com.cn/problem/P2840

我状态转移写错了:我认为 取w元=取w-ai+取ai元 方案数一样。

明显不对,ai是循环的,这个方案数会乱变。

mi+=mi-a\[i]才行,循环减去各个面额的硬币,看看减去后的方案数,累加。

cpp 复制代码
#include<bits/stdc++.h>
using namespace std;
//与顺序有关
//所有方式-DFS
//完全背包?
long long m[10005];
int a[1005];
const int MOD = 1e9 + 7;
int main(){
    int n,w;
    cin>>n>>w;
    for(int i=1;i<=n;i++){
        cin>>a[i];
    }
    m[0]=1;
    for(int i=1;i<=w;i++){
        for(int j=1;j<=n;j++){
            if(i>=a[j]){
                m[i]=(m[i]+m[i-a[j]])%MOD;
            }
        }
    }
    cout<<m[w]<<endl;
}

【DP】最长上升子序列 P3637

方法一:考虑转移方程

难点在于状态方程:

这里可以得到是 比当前节点小、且下标也小 的连续值+1

https://www.luogu.com.cn/problem/B3637

cpp 复制代码
#include<bits/stdc++.h>
using namespace std;
int n;
int F[1000005];
int a[1000005];

int main(){
    int max_len=0;
    cin>>n;
    for(int i=1;i<=n;i++){
        cin>>a[i];
    }
    F[0]=F[1]=1;
    for(int i=1;i<=n;i++){
        int max_pre=0;
        for(int j=1;j<=i;j++){
            if(a[j]<a[i]){
                max_pre=max(max_pre,F[j]);
            }
        }
        F[i]=max_pre+1;
        max_len=max(max_len,F[i]);
    }
    cout<<max_len<<endl;
}

方法二:考虑转换状态定义

cpp 复制代码
#include<bits/stdc++.h>
using namespace std;
//转移规则:判断其与F[curcnt]的大小,如果大,就加在后面 F[curcnt++]=cur;如果不可以,找出F数组中第一个大于等于cur的位置。二分。


int a[5005];
int F[5005];
int curcnt=0;

int main(){
    int n;
    cin>>n;
    F[0]=0;
    for(int i=1;i<=n;i++){
        cin>>a[i];
        if(a[i]>F[curcnt]) {
            F[++curcnt]=a[i];
        }else{
            int L=0,R=curcnt;
            while(R>L){
                int M=(L+R)/2;
                if(F[M]>=a[i]){
                    R=M;
                }else{
                    L=M+1;
                }
            }
            F[L]=a[i];
        }
    }
    cout<<curcnt<<endl;
}

最长公共子序列

任意两串 T446233

https://www.luogu.com.cn/problem/T446233

状态定义:Fij为X序列前i个和Y序列前j个元素中重合的数量

初始化:F0j=0 F0i=0;

目标状态:FNM

转移方程:如果XY下一个相同,那么变为Fij=Fi-1j-1+1;

否则 如果X+1与Y的公共子序列更长,则选他,如果X与Y+1的公共子序列更长,则选他。

cpp 复制代码
#include<bits/stdc++.h>
using namespace std;
int F[1005][1005];
int X[1005],Y[1005];
int main(){
    int N,M;
    cin>>N>>M;
    for(int i=1;i<=N;i++){
        cin>>X[i];
    }
    for(int i=1;i<=M;i++){
        cin>>Y[i];
    }
    for(int i=0;i<=N;i++){
        for(int j=0;j<=M;j++){
            if(i==0||j==0){
                F[i][j]=0;
            }else if(X[i]==Y[j]){//如果末尾相同
                F[i][j]=F[i-1][j-1]+1;
            }else{
                F[i][j]=max(F[i][j-1],F[i-1][j]);
            }
            
        }
    }
    cout<<F[N][M];
}

数量相同且不重复的两串 P1439

https://www.luogu.com.cn/problem/P1439

但是这样时间复杂度太高了。对于已知范围且相同数量的两个序列,可以用重映射的方式。

记f为Y的下标序列,将这组关系映射到X下,(5,1)(3,2)(4,3)(1,4)(2,5)=》2 5 4 3 1

就变成对1 2 3 4 5与2 5 4 3 1求公共子序列了。

也就是求25431的最长上升序列。

用Map映射是最好的方式

cpp 复制代码
#include<bits/stdc++.h>
using namespace std;
//用Map存映射
int X[100005],Y[100005];
map<int,int> m;
int F[100005];
int N;
int curcnt=0;
int main(){
    cin>>N;
    for(int i=1;i<=N;i++){
        cin>>X[i];
        m[X[i]]=i;//绑定{X[1],1}{X[2],2}
    }
    for(int j=1;j<=N;j++){
        int t;
        cin>>t;
        auto it=m.find(t);
        Y[j]=it->second;
    }
    //Y现在是一个最长升序问题
    //转移规则:判断其与F[curcnt]的大小,如果大,就加在后面 F[curcnt++]=cur;如果不可以,找出F数组中第一个大于等于cur的位置。二分。
    F[0]=0;
    for(int i=1;i<=N;i++){
        if(Y[i]>F[curcnt]){
            F[++curcnt]=Y[i];
        }else{
            int L=1,R=curcnt;//在之前的下标找
            while(R>L){
                int M=(R+L)/2;
                if(F[M]>=Y[i]){
                    R=M;
                }else{
                    L=M+1;
                }
            }
            F[L]=Y[i];
        }
        
    }
    cout<<curcnt;
}

线性DP练习小结

最优解问题------DP

最优解问题+单调------二分

先假设是DP问题,然后找是否满足:

注意DP的复杂度!

如果数据给到10^6,就是需要O(NlogN)的算法

背包问题

01背包

如果有多个状态,用struct Node存储,不要用现成的数据结构。

空间优化------逆向滚动数组

从后向前覆盖

cpp 复制代码
    for(int i=1;i<=r;i++){
        for(int j=i;j>=1;j--){
            int t;
            cin>>t;
            F[j]=max(F[j],F[j-1])+t;
        }
    }
cpp 复制代码
for(int i=1;i<=M;i++){//对于每一棵草都判断(对每一件物品判断)
        for(int t=T;t>=0;t--){(如果装得下(倒着装))
            if(t>=herb[i].t){
                F[t]=max(F[t],F[t-herb[i].t]+herb[i].v);(取或不取)
            }
        }
    }

P1048 采药问题

https://www.luogu.com.cn/problem/P1048

cpp 复制代码
#include<bits/stdc++.h>
using namespace std;

//时间、价值、没有数量限制,最多100
//时间是容量 价值是价值 如果如果100助都能采集完就输出100,如果不能就是01背包

struct Node{
    int t,v;
};
int T,M;
Node herb[105];
int F[1005];
int main(){
    cin>>T>>M;
    for(int i=1;i<=M;i++){
        cin>>herb[i].t>>herb[i].v;
    }
    F[0]=0;
    for(int i=1;i<=M;i++){//对于每一棵草都判断
        for(int t=T;t>=0;t--){
            if(t>=herb[i].t){
                F[t]=max(F[t],F[t-herb[i].t]+herb[i].v);
            }
        }
    }
    cout<<F[T];
}

常数优化

实际上如果背包很大,那么后面如果时间足够就不需要算了,比如还剩250秒,还剩20颗,这20颗的花费时间是200秒,那之间用F40计算输出即可。在循环中当时间t大于采集完后面所有草药和的时候,就直接全选跳出。

用一个逆序后缀和存储.

练习 P2925

https://www.luogu.com.cn/problem/P2925

cpp 复制代码
#include<bits/stdc++.h>
using namespace std;
int C,H;
int hay[50005];
int F[50005];
//F[j]存容积为i时装的最大体积
//hay[i]存每个稻草的体积
int main(){
    cin>>C>>H;
    for(int i=1;i<=H;i++){
        cin>>hay[i];
    }
    for(int i=1;i<=H;i++){
        for(int j=C;j>=hay[i];j--){
            F[j]=max(F[j],F[j-hay[i]]+hay[i]);//不选则不变 选则容积减少hay[i]
        }
    }
    cout<<F[C];
}

完全背包

转移方程:

取这个类------取一件,取完还能取这个类! Fiv-vi+wi 下次还能取i

不取这个类------Fi-1v

空间优化------正向滚动数组

cpp 复制代码
for(int i=1;i<=m;i++){
        for(int j=herb[i].t;j<=T;j++){
            F[j]=max(F[j],F[j-herb[i].t]+herb[i].v);   
        }
    }

练习 P1616

https://www.luogu.com.cn/problem/P1616

cpp 复制代码
#include<bits/stdc++.h>
using namespace std;
struct Node{
    int t,v;
};
int presum[10005];
Node herb[10005];//草药的种类10005 herb存草药的时间和价值
int T,m;
long long F[100000005];//记录F[i]时间下最大价值
int main(){
    cin>>T>>m;
    for(int i=1;i<=m;i++){
        cin>>herb[i].t>>herb[i].v;
    }
    for(int i=1;i<=m;i++){
        for(int j=herb[i].t;j<=T;j++){
            F[j]=max(F[j],F[j-herb[i].t]+herb[i].v);   
        }
    }
    cout<<F[T];
}

练习 P2722

https://www.luogu.com.cn/problem/P2722

cpp 复制代码
#include<bits/stdc++.h>
using namespace std;
struct Node{
    int t,s;
};
Node Q[10005];
int m,n;
int F[100005];
int main(){
    cin>>m>>n;
    for(int i=1;i<=n;i++){
        cin>>Q[i].s>>Q[i].t;
    }
    for(int i=1;i<=n;i++){
        for(int j=Q[i].t;j<=m;j++){
            F[j]=max(F[j],F[j-Q[i].t]+Q[i].s);
        }
    }
    cout<<F[m];
}

多重背包

k就是放多少件,不放就是0,最多放pi

对于当前物品,只有取k件然后进入下一个物品的选择。

练习 P1776

https://www.luogu.com.cn/problem/P1776

空间优化------逆向滚动数组

cpp 复制代码
#include<bits/stdc++.h>
using namespace std;
int n,W;
struct Node{
    int v,w,m;
};
Node P[105];
int F[100005];//存当前空间下的最大价值
int main(){
    cin>>n>>W;
    for(int i=1;i<=n;i++){
        cin>>P[i].v>>P[i].w>>P[i].m;
    }
    for(int i=1;i<=n;i++){//对于每类宝物
        for(int j=W;j>=0;j--){
            for(int k=0;k<=P[i].m&&j>=k*P[i].w;k++){
                F[j]=max(F[j],F[j-k*P[i].w]+k*P[i].v);   
            }
        }
    }
    cout<<F[W];
}

时间优化------二进制拆分

要x件可以拆为一堆二进制堆,看看要不要这一堆,就变成了logn的01背包问题

注意定义,下面的模板 w是价值 v是空间。和我习惯相反。下下面是我的模板。

cpp 复制代码
#include<bits/stdc++.h>
using namespace std;
int n,W;
int cnt=1;
int w[105],v[105],m[105];
int w1[100005],v1[100005];
int F[100005];//存当前空间下的最大价值
int main(){
    cin>>n>>W;
    for(int i=1;i<=n;i++){
        cin>>v[i]>>w[i]>>m[i];
        int temp=1;
        while(temp<=m[i]){//二进制拆分 当temp<=m[i] 即二进制数<个数时
            w1[cnt]=temp*w[i];//把temp个物品的重量合并,塞进w1中
            v1[cnt]=temp*v[i];//把temp个物品的价值合并,塞进v1中
            cnt++;
            m[i]-=temp;//从i类物品数量中减去拆走的部分
            temp<<=1;
        }
        w1[cnt]=m[i]*w[i];//剩余的重量放一堆
        v1[cnt]=m[i]*v[i];//剩余的价值放一堆
        cnt++;//下一类物品继续拆
        
    }
    //一定要注意,后面转换为01背包后 用的变量都是w1 v1修改后的内容了。
    for(int i=1;i<=cnt;i++){
        for(int j=W;j>=w1[i];j--){
            F[j]=max(F[j],F[j-w1[i]]+v1[i]);
        }
    }
    cout<<F[W];
}

混合背包

三种背包的初始状态 状态定义 最终目的都是一样的,唯一不同的是转移方程。

因此可以提前判断,然后根据不同的条件写for,从前往后从后往前滚动数组。

另一种想法是,混合背包就是多重背包,对于01背包本身就是多重的一种,完全背包也不是无限的,而是有界的,最多装:背包大小/物品大小。

处理链:【混合->多重->二进制拆分->01背包】

模板 P1833 樱花

处理链:【混合->多重->二进制拆分->01背包】

https://www.luogu.com.cn/problem/P1833

使用 cin 配合字符变量

cin,可以定义一个 char 变量专门用来"吃掉"冒号。

cpp 复制代码
int h1, m1, h2, m2, n;
char c; // 用来接收冒号
cin >> h1 >> c >> m1 >> h2 >> c >> m2 >> n;
cpp 复制代码
#include<bits/stdc++.h>
using namespace std;
int w[100005],v[100005],P[100005];
int w1[100005],v1[100005];
//n是数量 T是空间
int F[100005];
int cnt=1;
int main(){
    int h1,h2,m1,m2,n,T;
    char c;
    cin>>h1>>c>>m1>>h2>>c>>m2>>n;
    T=abs(h1*60+m1-h2*60-m2);
    for(int i=1;i<=n;i++){
        int p;
        cin>>w[i]>>v[i]>>p;
        if(p==0){
            P[i]=T/w[i];//最大容量/当前物品体积=最大数量
        }else{
            P[i]=p;
        }
        int temp=1;
        while(temp<=P[i]){
            w1[cnt]=temp*w[i];
            v1[cnt]=temp*v[i];
            cnt++;
            P[i]-=temp;
            temp<<=1;
        }
        w1[cnt]=P[i]*w[i];
        v1[cnt]=P[i]*v[i];
        cnt++;
    }
    for(int i=1;i<=cnt;i++){
        for(int j=T;j>=w1[i];j--){
            F[j]=max(F[j],F[j-w1[i]]+v1[i]);
        }
    }
    cout<<F[T];

}

区间动态规划 P1775

模板思路:

如果确定是区间DP,状态就是FLR,但是有些题可能需要额外的状态标记。

https://www.luogu.com.cn/problem/P1775

合并石子是不一样的,同一级区间内部合并代价一致,但是不同层次的合并代价不同。

(2) (2,5) (2,5,1)(2,5,1,4)

(2)(2,5) (1) (1,4) (2,5,1,4)

那么如何DP呢?

找状态:问题是求一个区间的最小合并代价。

子问题是 某个区间L,R内的最小代价。 定义FLR为区间最小代价

然后找初始状态,FLL=0 FLR=INT_MAX

结束状态,F1N

转移方程:当前层的合并代价就是区间和,但是前一级的合并方式会影响代价。

看到Σ立马考虑区间和。前缀和。

方案一:记忆化深搜

无穷大的防溢出表达:0x3f

cpp 复制代码
#include<bits/stdc++.h>
using namespace std;
int presum[305];
int st[305];
int N;
int F[305][305];
//记录DFS:搜索中如果遇到计算过的直接跳过
int dfs(int L,int R){
    if(L==R){//如果区间长度为0 就0
        return 0;
    }else if(F[L][R]!=0x3f3f3f3f){//如果区间被计算过
        return F[L][R];
    }else{//否则迭代计算
        for(int i=L;i<R;i++){//遍历所有的分割点,找最小分割代价
            F[L][R]=min(F[L][R],dfs(L,i)+dfs(i+1,R)+presum[R]-presum[L-1]);   
        }
        return F[L][R];
    }
}

int main(){
    cin>>N;
    for(int i=1;i<=N;i++){
        cin>>st[i];
        presum[i]=presum[i-1]+st[i];
    }
    //设置整个F区间为INT_MAX 未被计算
    memset(F,0x3f,sizeof(F));
    cout<<dfs(1,N);
    
}

方案二:循环

cpp 复制代码
#include<bits/stdc++.h>
using namespace std;
int presum[305];
int st[305];
int N;
int F[305][305];

int main(){
    cin>>N;
    for(int i=1;i<=N;i++){
        cin>>st[i];
        presum[i]=presum[i-1]+st[i];
    }
    //设置整个F区间为INT_MAX 未被计算
    memset(F,0x3f,sizeof(F));
    for(int i = 1; i <= N; i++){
        F[i][i] = 0; 
    }
    for(int len=1;len<N;len++){//枚举长度
        for(int L=1;L<=N-len;L++){//枚举左端点
            int R=L+len;//右端点可以直接得到
            for(int k=L;k<R;k++){//遍历所有分割点
                F[L][R]=min(F[L][R],F[L][k]+F[k+1][R]+presum[R]-presum[L-1]);
            }
        }
    }
    cout<<F[1][N];
    
}

环形动态规划(遇到放弃)

断环为链

树形动态规划(遇到放弃)

倍增与快速幂

倍增

快速幂

拆指数为二进制,每次看二进制是否要乘上对应的a^(k-1)

模板

b每次通过&1获取最后一位,通过最后一位是否为1判断ans是否要乘上a^(k-1)。然后a倍增,b去掉最后一位。

取余不变性 P1226

https://www.luogu.com.cn/problem/P1226

cpp 复制代码
#include<bits/stdc++.h>
using namespace std;
long long a,b,p;
long long qpow(long long a,long long b){
    long long ans=1;
    // 先对底数取模,防止后续平方溢出
    a = a % p;
    while (b > 0) {
        // 如果 b 是奇数
        if (b & 1) {
            ans = (ans * a) % p;
        }
        // 底数平方,并立即取模(关键修正点)
        a = (a * a) % p;
        b >>= 1;
    }
    return ans;
}
int main(){
    cin>>a>>b>>p;
    cout<<a<<"^"<<b<<" mod "<<p<<"="<<qpow(a,b);
}

练习 P1965

https://www.luogu.com.cn/problem/P1965

cpp 复制代码
#include<bits/stdc++.h>
using namespace std;

long long n,m,k,x;
//(x+10^k*m)%n=x%n+(10^k*m)%n=x%n+((10^k)%n*m%n)%n
int main(){
    cin>>n>>m>>k>>x;
    long long ans=1;
    long long a=10;
    long long b=k;
    a=a%n;
    while(b){
        if(b&1){
            ans=(ans*a)%n;
        }
        a=(a*a)%n;
        b>>=1;
    }
    long long temp=x%n+(ans*(m%n))%n;
    cout<<temp%n<<endl;
}

ST表(用于查询 解决重复贡献问题 区间最值 区间GCD)

RMQ问题(区间多次询问)【区间动态规划】

扫描效率太低了,求最值就考虑动态规划,FLR为区间最大值,最大值可能是之前区间的最大值FLR-1和当前值aR,的最大值,每次外层for的时候记得输入FLL

cpp 复制代码
// 1. 枚举左端点 L
for (int L = 0; L < n; L++) {
    // 2. 初始化
    F[L][L] = a[L];
    
    // 3. 正着枚举右端点 R (从 L+1 到 n-1) -> 向右扩展
    for (int R = L + 1; R < n; R++) {
        F[L][R] = max(F[L][R-1], a[R]);
    }
}

但是效率还是太低O(N^2)。

考虑倍增。拼凑区间。比如长度为15的区间可以由1 2 4 8拼凑。

还可以优化,重复求取不会影响最值,15的区间可以用从0和7开始长度为8的区间拼凑,中间会重复1,但是没关系。

这样就能使用最多两个预处理区间完成O(1)的搜索。

ST表 P3865

https://www.luogu.com.cn/problem/P3865

两个过程:预处理-查询

预处理

定义Fij状态为以i为起点,长度为2^j的区间最大值,也就是i,i+2\^j-1区间的最大值。确保每次计算的都是2的幂次区间。

初始化时,Fi0以i为起点长度为1的区间最值就是本身ai

转移方程就是当前区间切两半找最大的值。

查询

拆为两个区间,长度是小于len的最大2次幂,左端点和右端点分别向内找这个区间。

cpp 复制代码
#include<bits/stdc++.h>
using namespace std;

//要预先处理logN表,存以2为底的下取整数 因为给定长度后要计算其下取证最大二次幂
int logN[100005];
int N,M;
int F[100005][18];
int main(){
    ios::sync_with_stdio(false);
    cin.tie(0);
    cin>>N>>M;
    cin>>F[1][0];
    for(int i=2;i<=N;i++){//对于
        cin>>F[i][0];//对于第i个长度为1的最大值就是自身
        logN[i]=logN[i>>1]+1;//logN[1]=1 logN[2]=logN[1]+1=2 logN[5]=logN[2]+1=3
    }
    //执行 预处理 计算各个位置各个长度的区间和
    for(int j=1;j<=logN[N];j++){
        for(int i=1;i + (1<<j) - 1 <= N;i++){//右边界=i+2^j-1<=N i能算出来
            
            F[i][j]=max(F[i][j-1],F[i+(1<<(j-1))][j-1]);
        }
    }
    int L,R;
    //执行 查询
    for(int i=1;i<=M;i++){
        cin>>L>>R;
        int loglen=logN[R-L+1];
        cout<<max(F[L][loglen],F[R-(1<<loglen)+1][loglen])<<"\n";
    }
}

线段树(穿插有修改和查询)

预处理区间,拼凑查询的区间。4,7就可以通过4,56,7拼凑出来。

存储上使用一个数组存储这个区间的最大值即可,不用存区间范围,因为范围可以求出来。

对于每个节点p而言,其最大值为其左右子节点的最大值dp=max(dp\<\<1,d(p\<\<1)\|1

采用类堆索引,p节点的子节点是p*2 p*2+1

数组要开多大?4N个

建树

采用类堆索引,p节点的子节点是p*2 p*2+1。

这个结构告诉我们,buildtree要传入三个参数 区间范围st和当前节点编号p

区间查询

输入是查询目标LR 查询区间是sm,如果有就直接返回。如果没有就看LR是否被m切为两半,如果有部分在左侧(L<=m)那么去左边递归找最大值,否则去右边找最大值。

复杂度分析

小结

线段树单点修改

线段树区间修改(懒标记)

区间如果下传到每个节点修改成本太高,考虑修改部分节点。

只用改两个节点,就是边界节点。其余包含在LR内的节点打上懒标记。【类似前缀和的思想,中间的非边界节点同时增加,最大值关系不变,只有边界节点最大值可能改变】

对于被包含在LR内部的最值,直接+=修改值v,打上懒标记lazyp+=v;

对于在边界的节点,比如修改3,7时的1,4区间,就包含了边界3.此时就需要跟新这个节点。

更新边界节点要递归更新其左右子树,最后再更新自身,自身依旧是左右子树取最大值。

其中这个push_down的函数就是将懒标记下传的意思。

将懒标记lazyp传递给左右子树,也就是对其dp\<\<1 d(p\<\<1)\|1+=lazyp加上当前节点的标记值。加完之后清空当前节点的标记值。

拓扑排序

拓扑排序的实现Kahn BFS实现 B3644

思路

addEdge建图时读入度,找第一个无依赖节点,加入Q队列,while探索Q队列,广搜u的邻接节点v,搜到就对v的入度减一,如果v入度为0就加入Q队列。

https://www.luogu.com.cn/problem/B3644

注意 拓扑问题一个节点可以有多个输出边,因此要一直输入

cpp 复制代码
    for(int i=1;i<=N;i++){
        //注意 拓扑问题一个节点可以有多个输出边,因此要一直输入
        while(cin>>v && v){
            cin>>v;
            addEdge(i,v);
            inE[v]++;
        }
    }
cpp 复制代码
#include<bits/stdc++.h>
using namespace std;

struct Edge{
    int v,next;
};
Edge E[20005];
int N;
int inE[20005];//记录各个节点的入读,入度为0即加入探索队列
int cur=1;
int fst[20005];
void addEdge(int u,int v){
    E[cur].v=v;
    E[cur].next=fst[u];
    fst[u]=cur++;
}
int main(){
    cin>>N;
    int v;
    for(int i=1;i<=N;i++){
        //注意 拓扑问题一个节点可以有多个输出边,因此要一直输入
        while(cin>>v && v){
            addEdge(i,v);
            inE[v]++;
        }
    }
    queue<int> q;
    //找到第一个度为0的节点 放入探索队列
    for(int i=1;i<=N;i++){
        if(!inE[i]) q.push(i);
    }
    //对当前节点u广搜
    while(!q.empty()){
        int u=q.front();
        cout<<u<<" ";
        q.pop();
        for(int p=fst[u];p;p=E[p].next){
            int v=E[p].v;
            inE[v]--;//搜索到这个节点后就减少其入度
            if(!inE[v]){//如果当前节点入度为0
                q.push(v);
            }
        }
    }
    return 0;
}

DFS实现

思路:

依次对节点进行dfs,搜过的节点vis就设为true,一个节点如果其所有子节点都搜索完后就将其入栈,最后出栈顺序就是拓扑排序。

练习

并查集

初始化 将根节点设为自己

查询根节点 判断是否属于统一集合(判断二者的根是否相同)

合并集合 (fafind(x)=findy挂x到y)

查找的复杂度太高了

路径压缩(查询优化)

直接把自己挂在根节点下,树就只有一层了。

原先通过find(fax)获取根节点,那么我们直接把父节点设为根节点:fai=find(fax)即可。

按秩合并 (合并优化)

将高度小的树合并到高度大的树里,防止增加高度。

也就是fafind(x)=find(y)时把x挂在y时x是低高度树。

创建一个记录各个根节点深度的数组,初始时自己就是根节点,深度为1.

然后合并的时候判断,如果深度不一致就把x低高度的挂在y高高度的树上。

如果深度一致,就随意,然后深度++;

总结模板

练习

矩阵运算

线段树(穿插有修改和查询)

这样就用不了

线段树基础

线段树结构

管理线段的数据结构,每个节点对应一个区间。

mid是向下取整 l+r/2

由于二分,因此覆盖某个区间1,n的数量一定要超过长度的一半。

单点加法 区间求和

加法就是从底层向上传递+k即可

第一种情况是L<=l r<=R,这种情况说明切割区间全部包含了,全部加上sumi即可。否则就递归切割

完整代码

只需要指导有前面的函数即可。

带标记线段树

懒标记 李超树 不会用到

查询到的时候再去往下修改,将修改的复杂度均摊到查询时,也就是写时复制。

懒标记 代码

区间乘和加

关键在于如何表达和下推标记

区间乘法 代码

树状数组(只能用于单点加法 区间求和)

lowbit

树状数组是多叉树

二维偏序

对于任何一个数,其逆序对的数量都等于前一个数的逆序对加上后面小于自身的数字个数

ST表

押题

合并启发式 树链 最近公共祖先

错误解决

RERuntime Error(运行时错误)的缩写。

简单来说,这意味着你的代码成功通过了编译 (语法没有错误),但在实际运行过程中,程序遇到了无法处理的情况,导致被操作系统或评测系统强制终止。

在算法竞赛(如 OJ 平台)中,RE 通常由以下几种原因引起:

1. 数组越界 (最常见)

这是新手最容易犯的错误,也是你上一段代码中存在的问题。

  • 现象:访问了数组范围之外的内存。
  • 例子 :定义 int a[10],却访问了 a[10](下标最大为 9)或 a[-1]
  • 后果:程序试图读写未分配的内存区域,操作系统会立即杀掉进程。

2. 除以零

  • 现象:代码中出现了除数为 0 的运算。
  • 例子int a = 5 / 0;int a = 5 / b;(此时 b 恰好为 0)。
  • 后果:CPU 无法执行该指令,触发异常。

3. 栈溢出

  • 现象:通常是递归太深,或者在函数内定义了过大的局部数组。
  • 例子 :递归函数没有正确的终止条件(死递归),或者写了 void solve() { int bigArray[1000000]; ... }
  • 后果:内存栈空间耗尽。

4. 指针错误 (C/C++)

  • 现象:访问了空指针或野指针。
  • 例子int *p = NULL; *p = 10;
cpp 复制代码
/tmp/compiler_7uvr2ihl/src:8:8: 错误:'int y1' redeclared as different kind of entity
    8 | int x1,y1,x2,y2;
      |        ^~
In file included from /nix/store/79624djlfdc0a6anji2rwqd9p9ycqi8h-glibc-2.34-210-dev/include/features.h:490,
                 from /nix/store/bbmwawbq7wjb54fa35wr72alcm083d1f-luogu-gcc-9.3.0/include/c++/9.3.0/x86_64-unknown-linux-gnu/bits/os_defines.h:39,
                 from /nix/store/bbmwawbq7wjb54fa35wr72alcm083d1f-luogu-gcc-9.3.0/include/c++/9.3.0/x86_64-unknown-linux-gnu/bits/c++config.h:524,
                 from /nix/store/bbmwawbq7wjb54fa35wr72alcm083d1f-luogu-gcc-9.3.0/include/c++/9.3.0/cassert:43,
                 from /nix/store/bbmwawbq7wjb54fa35wr72alcm083d1f-luogu-gcc-9.3.0/include/c++/9.3.0/x86_64-unknown-linux-gnu/bits/stdc++.h:33,
                 from /tmp/compiler_7uvr2ihl/src:1:
/nix/store/79624djlfdc0a6anji2rwqd9p9ycqi8h-glibc-2.34-210-dev/include/bits/mathcalls.h:224:1: 附注:previous declaration 'double y1(double)'

这是redeclared错误,要么自己命名重复,要么可能和库变量重名了。

如果是撞库了,直接移到main内即可。

还有一种可能,就是数组开的不够,或者爆int了,开大一些,并改为long long即可

缺少默认实参

cpp 复制代码
struct Node {
    int x1, y1, x2, y2, lay;
    
    // ❌ 错误写法:前4个有默认值,第5个没有
    Node(int a=0, int b=0, int c=0, int d=0, int e) 
        : x1(a), y1(b), x2(c), y2(d), lay(e) {}
};

相关推荐
CQU_JIAKE1 小时前
6.1【A】
算法
wayz111 小时前
Momentum:CTI(相关趋势指标)技术指标详解
算法·金融·数据分析·量化交易·特征工程
fengxin_rou1 小时前
【滑动窗口与前缀和算法实战】:LeetCode560.438 高频题深度解析
java·算法·leetcode
Dillon Dong1 小时前
【风电控制】FPGA vs DSP 在ADC采样中的选择——从架构差异到工程实践
算法·变流器·风电控制·dfig
科研小白_1 小时前
【第九期:MATLAB点云处理基础】基于 Alpha Shapes 的边缘点提取
算法
sali-tec1 小时前
C# 基于OpenCv的视觉工作流-章80-长短脚
图像处理·人工智能·opencv·算法·计算机视觉
sul.i1 小时前
浅析·指针
算法
春日见1 小时前
策略梯度算法
算法
Brilliantwxx1 小时前
【算法从零到千】【1-7】 双指针算法
开发语言·c++·笔记·算法·leetcode·推荐算法