程序设计竞赛
文章目录
- 程序设计竞赛
 - 0x00 基本操作指南
 - 0x01 算法分析
 - 0x02 STL和基本数据结构
 - 栈
 - 队列
 - 集合
 - map
 
- 0x03 排序
 - 插入排序
 - 归并排序(Merge Sort)
 - 快速排序
 
- 0x04 搜索技术
 - BFS
 - DFS
 - 回溯与剪枝
 
- 深度迭代
 - ID A*
 - A star
 - 双向广搜
 
- 0x05 递推方程
 - 0x06 高级数据结构
 - 并查集
 - 二叉树
 - 二叉搜索树
 - 退化树
 - Treap树(堆)
 - 伸展树Splay
 - 线段树
 - 树状数组
 
- 0x07 分治法*
 - 一般方法
 - 求最大最小元
 - 二分搜索
 - 排序问题
 - 选择问题
 - 斯特拉森
 
- 0x08 动态规划(Dynamic Programming,DP)*
 - 定义
 - 硬币组合
 - 0/1 背包问题
 - 滚动数组
 - 最短路径问题
 - 最长公共子序列(LCS)
 - 最少拦截问题 => LIS 最长递增子序列
 - 矩阵连乘问题
 
- 0x09 基础语法
 - 0x10 二分答案
 - 概念
 - 练习
 - 整数域二分
 - 实数域二分
 
- 0x11 贪心法(Greedy)*
 - 导言
 - 背包问题
 - 0/1 背包问题
 - 一般背包问题
 
- 活动安排问题
 - 最佳合并模式
 
- 0x12 数论
 - 质数
 - 快速幂
 - 矩阵快速幂
 - 约数
 - 扩欧算法
 
- 0x13 图论
 - 基本概念
 - 无向图和有向图的连通性
 
- 0x14 回溯法*
 - 基本概念
 - 涂色问题
 - 4/n-皇后问题
 - 0/1背包问题
 
- 0x15 分支限界法*
 - 4-皇后问题
 
- 0x16 优先队列
 
0x00 基本操作指南
输入的结束,推荐使用下面的非~:位运算符,按位取反;只有当x值为-1时(-1的补码:11111111),~x的值才为0
while(~scanf("%d%d", &a, &b))
 
指定数据个数结束,提高程序编写效率
int main(){
    int n, a, b;
    scanf("%d", &n);
    while(n -- ){
        
    }
}
 
以特定元素作结束符,以读取到0结束为例
while(~scanf("%d", &n) && n)   
 
-  
测试的两种方法:
- 在程序中加入测试代码
 
#include<stdio.h> #define mytest int main(){ int n; #ifdef mytest freopen("test.in", "r", stdin); //将stdin重定向到test.in文件,file redirect 不需要从键盘输入了 //freopen("test.out", "w", stdout); #endif while(~scanf("%d", &n) && n){ printf("%d\n", n * n); } return 0; }- 在命令行中重定向 好处在于不需要修改源代码适合测多个文件
 
#include<stdio.h> int main(){ int n; while(~scanf("%d", &n) && n){ printf("%d\n", n * n); } return 0; }上面文件abc.cpp 一定注意要把源文件进行编译 生成exe文件
然后在相同位置 创建test.in文件 记事本
在该位置进入到当前目录 执行命令
abc < test1.in > test1.out
<: 输入>: 输出重定向把结果存入到test1.out

然后对比我们的结果和答案(答案是我们预测的结果)是否相同
fc test1.out answer.out
命令进阶版
创建xxx.bat文件 批量化执行命令脚本
abc <test1.in> test1.out abc <test2.in> test2.out abc <test3.in> test3.out fc test1.out answer1.out fc test2.out answer2.out fc test3.out answer3.out pause 
提速:
- 使用STL库
 - 不使用动态调试 而是通过打印中间信息
 - 模板
 
算法证明 :
- 归纳法
 - 反证法
 
在c++ 中引入c的函数进行提速文件头
#include<iostream>
#include<cstdio>
#include<cmath>
using namespace std;
int main(){
    
    return 0;
}
 
- 题目类型: 
  
- Ad Hoc,杂题;
 - Complete Search(Iterative/Recursive),穷举搜索
 - Divide and Conquer,分治法
 - Greedy,贪心法
 - Dynamic Programming,动态规划
 - Graph,图论
 - Mathmatics,数学
 - String Processing,字符串处理
 - Computation Geometry,计算几何
 - Some Harder,罕见问题
 
 - 代码规范 
  
- 少用
 
 
课本推荐的题目全部要做掉 没时间做来上课干嘛
int最大:2147483647 20多个亿
- 报错:
 
RE:Runtime Error 运行错误 大部分都是数组越界 特殊:除0越界
MLE:数组开太大
- 区分scanf和cin的区别
 
主要是在输入一个数字+一段字符时容易出错
cin cout 提速代码: 数据达到百万级 注意:如果进行了加速 scanf 和printf就必须禁用
ios::sync_with_stdio(flase);
cin.tie(0);
cout.tie(0);
 
- bug 避坑
 
如果创建副本 会导致执行相同的exe文件 所以需要修改前面的命名
- 大数据测试方法:
 
记事本打不开 直接死掉
使用fopen
freopen("1.in","r",stdin);
//输出
freopen("1.txt", "w", stdout);
 
-  
数组建议从1开始 因为在搜索的时候 地图如果从0开始可能有问题
 -  
double 类型的数据 在读入的时候是lf 但是输出本身就是f
 
0x01 算法分析
- 多项式复杂度
 - 指数复杂度(一般是暴力求解出现)
 
O ( 2 n ) < O ( n ! ) < O ( n n ) O(2^n) < O(n!) < O(n^n) O(2n)<O(n!)<O(nn)
最快的方法:打表法
只输出结果
打表法:以素数个数求解
float Sum(float list[], const int n){
    float tempsum = 0.0;
    count ++;
    for(int i = 0; i < n; i++){
        count ++;
        tempsum += list[i];
        count ++;
    }
    count ++;
    count ++;
    return tempsum;
}
 
确定问题的规模 根据count ++的数量 T(n) = 2 * n + 3
f ( n ) = 2 n + 3 = O ( n ) 当 n ≥ 3 时 f(n) = 2n +3 = O(n)\\ 当n\ge3时 f(n)=2n+3=O(n)当n≥3时
一点一点放
  
      
       
        
        
          10 
         
         
         
           n 
          
         
           2 
          
         
        
          + 
         
        
          4 
         
        
          n 
         
        
          + 
         
        
          2 
         
         
        
          当 
         
        
          n 
         
        
          ≥ 
         
        
          2 
         
        
          时,原式 
         
        
          ≤ 
         
        
          10 
         
         
         
           n 
          
         
           2 
          
         
        
          + 
         
        
          5 
         
        
          n 
         
         
        
          当 
         
        
          n 
         
        
          ≥ 
         
        
          5 
         
        
          是,上式 
         
        
          ≤ 
         
        
          10 
         
         
         
           n 
          
         
           2 
          
         
        
          + 
         
         
         
           n 
          
         
           2 
          
         
        
          = 
         
        
          11 
         
         
         
           n 
          
         
           2 
          
         
        
          = 
         
        
          > 
         
        
          O 
         
        
          ( 
         
         
         
           n 
          
         
           2 
          
         
        
          ) 
         
        
       
         10n^2 + 4n +2\\ 当n\ge2时,原式\le10n^2 +5n \\ 当n\ge5是,上式\le10n^2+n^2 = 11n^2 =>O(n^2) 
        
       
     10n2+4n+2当n≥2时,原式≤10n2+5n当n≥5是,上式≤10n2+n2=11n2=>O(n2)
反证法
  
      
       
        
        
          10 
         
         
         
           n 
          
         
           2 
          
         
        
          + 
         
        
          9 
         
        
          ≠ 
         
        
          O 
         
        
          ( 
         
        
          n 
         
        
          ) 
         
        
       
         10n^2 + 9 \neq O(n) 
        
       
     10n2+9=O(n)
- 矩阵乘法
 
for(i = 0; i < n; i++){
    for(j = 0; j < n; j++){
        c[i][j] = 0;
        for(k = 2; k < n; k++){
            xxx
            ...
            xxx
        }
    }
}
 
O(n次方)
主方法:
理解与应用
证明不会重点考查
除法相对于其他运算会非常慢 所以一般采用移位进行做除法
增长率计算方法:https://blog.csdn.net/qq_37657182/article/details/102826437


0x02 STL和基本数据结构
- STL
 
Standard Template Library,C++的标准模板库,竞赛中常用的数据结构和算法可以直接调用。
容器:存放数据的,如Vector list queue stack map(映射) set(集合)
 容器适配器:在别的容器的基础上加上外壳 把原有的容器的基础上做一些限制和处理 如stack queue priority_queue
 关联容器:set map multiset(关键词可以重复) multimap
 无序关联容器:真正使用哈希表 从c++11开始 在关联容器的前面+unordered_
 通用功能:
 begin() 存在数据 end() 无数据
迭代器:容器和算法之间的桥梁,可以认为是一个特殊的指针,指明数据的来源和去处
算法:如sort
函数对象:谓词,比如在排序的时候按照从大到小还是从小到大 利用函数对象进行说明
在竞赛中尽量不用vector 推荐数组 Vector为动态数组
栈
hdu 1237 自己做
队列
大量删除操作 绝对别用vector
集合
set和map 本质是红黑树 Red Black Tree
本身是有序的
1 3 5 7 9
使用必须有序
A.lower_bound(k) x>=
A.upper_bound() x >
hdu 2094
凡是选手进第一个集合
失败者进第二个集合
最终比较两个集合的差 如果差是1 产生冠军 否则没有冠军
map
关联容器 键值对 key => value 映射
hdu 2648
homework
提交解题报告 用到哪些原理 基础知识 算法 源程序
每一个程序至少5个测试数据 需要大规模测试数据 不能常规的
0x03 排序
插入排序
打扑克牌的插入方法,在部分有序的数列中不断插入新的元素
//c++
template<class Type>   //把类型抽象成参数  不再依赖于具体的数据类型
void insertion_sort(Type *a, int n){
    Type key;
    for(int i = 1; i < n; i++){
        key = a[i];
        int j = i - 1;
        while(j >= 0 && a[j] > key){
            a[j + 1] = a[j];
            j--;
        }
        a[i + 1] = key;
    }
}
 
归并排序(Merge Sort)
快速排序
0x04 搜索技术
BFS DFS 深搜 广搜
本质就是暴力搜索,走投无路就暴搜
广搜索:一层一层拨开,先离我距离为1的 再距离我为我2的 采用队列!
BFS
宽搜:绝对不要自己手写队列
/*
  来存一个BFS的模板 
*/ 
int dx[] = {1, 0, -1, 0};
int dy[] = {0, 1, 0, -1};
int vis[1005][1005];   //做标记 是否访问过 
struct P{
    int x,y,z;
};
bool BFS(int x, int y, int mid){
	queue<P> q;
	q.push({x, y, a[x][y]});
	while(!q.empty()){
		P x = q.front();
		q.pop();
		//
		if(a[x.x][x.y] <= mid){
			//如果走到头了   直接返回true 
			if(x.x == n) return true;
			//没走到头 继续往下走
			for(int k = 0; k < 4; k++){
				int nx = x.x + dx[k];
				int ny = x.y + dy[k];
				//矩阵范围判断 
				if(nx >= 1 && ny >= 1 && nx <= n && ny <= m && vis[nx][ny] == 0 && a[nx][ny] <= mid){
					q.push({nx, ny, max(x.z, a[nx][ny])});
					vis[nx][ny] = 1; //表示访问过了 
				}
			} 
			 
		} 
	}
    return false; 
}
 
DFS
深搜:自己手写栈
回溯与剪枝
如果明确知道某个分支下面不可行 则不再向下走
八皇后问题:
不在一个斜线:
  
      
       
        
        
          ∣ 
         
        
          i 
         
        
          − 
         
        
          j 
         
        
          ∣ 
         
        
          = 
         
        
          ∣ 
         
         
         
           x 
          
         
           i 
          
         
        
          − 
         
         
         
           x 
          
         
           j 
          
         
        
          ∣ 
         
        
       
         |i - j| = |x_i - x_j| 
        
       
     ∣i−j∣=∣xi−xj∣
 剪枝的过程中要不断check
bool check(int c, int r){ for(int i = 0; i < r; i++){ if(col[i] != c && abs(i - r) != abs(col[i] - c)){ } } }
深度迭代
ID A*
A star
曼哈顿启发式算法
  
      
       
        
        
          ∣ 
         
         
         
           x 
          
         
           1 
          
         
        
          − 
         
         
         
           x 
          
         
           2 
          
         
        
          ∣ 
         
        
          + 
         
        
          ∣ 
         
         
         
           y 
          
         
           1 
          
         
        
          − 
         
         
         
           y 
          
         
           2 
          
         
        
          ∣ 
         
        
       
         |x_1 - x_2| + |y_1 - y_2| 
        
       
     ∣x1−x2∣+∣y1−y2∣
双向广搜
0x05 递推方程
分解问题的规模,以及问题的相关性
列出递推方程进行求解问题
-  
必须有一个初始条件
 -  
计算时的常用三种方法:
-  
替换法:首先猜测递推式的解,然后使用归纳法证明,如解决汉诺塔问题
T ( n ) = { 1 n = 1 2 T ( n − 1 ) + 1 n > 1 T(n)= \begin{cases} 1 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~n=1\\ 2T(n-1) + 1~~~~~~~~n>1 \end{cases} T(n)={1 n=12T(n−1)+1 n>1void Hanoi(int n, tower x, tower y, tower z){ if(n){ Hanoi(n-1, x, y, z); Move(n, x, y); Hanoi(n-1, z, y, x); } } -  
迭代法:针对T(n)进行推导 不带入具体数据
 -  
主方法:
 
 -  
 
递归树:把中间的节点一分为2
0x06 高级数据结构
并查集
指的是不想交的集合 disjoint
经典应用:连通子图、最小生成树Kruskal算法 最近共同祖先
实现代码:
int parent[105];
// 并查集初始化
void UFset() {
    for (int i = 1; i <= n; i++) {
        parent[i] = -1;
    }
}
int find(int i) {
	//update the value of temp
    int temp;
    for (temp = i; parent[temp] >= 0; temp = parent[temp]);
    
    while (temp != i) {
        int t = parent[i];
        parent[i] = temp;
        i = t;
    }
    return temp;
}
// 合并两个集合
void merge(int a, int b) {
    int r1 = find(a);
    int r2 = find(b);
    int tmp = parent[r1] + parent[r2];
    if (parent[r1] > parent[r2]) {
        parent[r1] = r2;
        parent[r2] = tmp;
    } else {
        parent[r2] = r1;
        parent[r1] = tmp;
    }
}
// Kruskal算法求最小生成树的总权值
int kruskal() {
    int sumWeight = 0;
    UFset(); // 初始化并查集
    for (int i = 1; i <= m; i++) {
        int u = edges[i].u;
        int v = edges[i].v;
        // 如果 u 和 v 不在同一个集合中,选择该边
        if (find(u) != find(v)) {
            sumWeight += edges[i].w;
            merge(u, v); // 合并 u 和 v 所在的集合
            cnt++;
        }
        // 已选择的边数达到 n-1 条时,退出循环
        if (cnt == n - 1) {
            break;
        }
    }
    return sumWeight;
}
 
二叉树
后序遍历:
先往下走 先左后右 无右向上
二叉搜索树
BST,比左孩子大,比右孩子小
红黑树是其中一种 set和map就是基于红黑树的一种
Treap树:在二叉搜索树的基础上增加了优先级
意味着成为一个树堆(大根堆、小根堆)
旋转:
void rotate(Node* &o, int d){
    Node *k = o -> son[d ^ 1];
    o 
}
 
注意son[0] 表示左孩子
son[1] 表示右孩子
在竞赛中尽量别用平衡二叉树 太繁琐
退化树
Treap树(堆)
插入:
删除:
struct 可以视为一个类
#include <iostream>
using namespace std;
struct Node{
    int x, y;
    Node(int x, int y){
        this -> x = x;
        this -> y = y;
    }
    void print(){
        cout << x << y;
    }
}
//或者真正用类  但是注意其他的方法都是private 所以需要先声明权限
class Node{
    public:
    int x, y;
    Node(int x, int y){
        this -> x = x;
        this -> y = y;
    }
    //或者:Node(int x, int y):x(x),y(y){}
    void print(){
        cout << x << y;
    }
}
int main(){
    
}
 
伸展树Splay
不要求树是平衡的 允许把任何结点放到根上去
单旋
三点共线旋
三点不共线旋
线段树
考查较少
树状数组
往往和折半查找混合在一起用
0x07 分治法*
一般方法
分-将要求解的较大规模的问题分割成k个更小规模的子问题 注意子问题和子问题之间是独立的
合-将求解出的小规模的问题的解合并为一个更大规模问题的解,自底向上逐步求出原来问题的解
SolutionType DandC(ProblemType P){
    ProblemType P1, P2,..., Pk;
    if(Small(P)) return S(P);
    else{
        Divide(P, P1, P2, P3, ... , Pk);
        Return Combine(DandC(P1), DandC(P2), ... , DandC(Pk));
    }
}
 
求最大最小元
说白了就是求最大值和最小值
传统法:规模不大可以用
maxmin(float a[], int n){
    max = min = a[0];
    for(int i = 1; i < n; i++){
        if(max < a[i]) max = a[i];
        else if(min > a[i]) min = a[i];
    }
}
 
分治法:
构造树,每个子树的范围为除以二
扩展:求次最大元 次最小元
#include <iostream>
#include <climits>
using namespace std;
const int N = 100;
float a[N];
void maxmin(int left, int right, float &max, float &second_max, float &min, float &second_min) {
    if (left == right) {
        max = a[left];
        second_max = INT_MIN;
        min = a[left];
        second_min = INT_MAX;
    } else if (left + 1 == right) {
        if (a[left] > a[right]) {
            max = a[left];
            second_max = a[right];
            min = a[right];
            second_min = a[left];
        } else {
            max = a[right];
            second_max = a[left];
            min = a[left];
            second_min = a[right];
        }
    } else {
        int mid = left + (right - left) / 2;
        float lmax, left_second_max, lmin, left_second_min;
        float rmax, right_second_max, rmin, right_second_min;
        maxmin(left, mid, lmax, left_second_max, lmin, left_second_min);
        maxmin(mid + 1, right, rmax, right_second_max, rmin, right_second_min);
        // Update max
        if (lmax > rmax) {
            max = lmax;
            // Check if rmax is greater than left_second_max or not
            if (rmax > left_second_max) {
                second_max = rmax;
            } else {
                second_max = left_second_max;
            }
        } else {
            max = rmax;
            // Check if lmax is greater than right_second_max or not
            if (lmax > right_second_max) {
                second_max = lmax;
            } else {
                second_max = right_second_max;
            }
        }
        // Update min
        if (lmin < rmin) {
            min = lmin;
            // Check if rmin is less than left_second_min or not
            if (rmin < left_second_min) {
                second_min = rmin;
            } else {
                second_min = left_second_min;
            }
        } else {
            min = rmin;
            // Check if lmin is less than right_second_min or not
            if (lmin < right_second_min) {
                second_min = lmin;
            } else {
                second_min = right_second_min;
            }
        }
    }
}
int main() {
    int n;
    cin >> n;
    for (int i = 0; i < n; i++) {
        cin >> a[i];
    }
    float max, second_max, min, second_min;
    maxmin(0, n - 1, max, second_max, min, second_min);
    cout << max << " " << second_max << endl;
    cout << second_min << " " << min << endl;
    return 0;
}
 
二分搜索
每次砍一半,效率较高
扩展:三分搜索
#include <iostream>
using namespace std;
/*
三分搜索
*/
const int N = 101;
int a[N];
int n;
int ternarySearch(int find){
	int left = 0;
	int right = n - 1;
	
	while(left <= right){
		int mid1 = left + (right - left) / 3;
		int mid2 = right - (right - left) / 3;
		
		//给出结果 
		if(a[mid1] == find){
			return mid1;
		}
		if(a[mid2] == find){
			return mid2;
		}
		
		//调整查找范围
		if(find < a[mid1]){
			right = mid1 - 1;
		} 
		else if(find > a[mid2]){
			left = mid2 + 1;
		}
		//在中间 
		else{
			left = mid1 + 1;
			right = mid2 - 1;
		}
		
	}
	return -1; 
}
int main(){
	cin >> n;   
	for(int i = 0; i < n; i++){
		cin >> a[i];
	}
	int find;
	cin >> find;
	int re = ternarySearch(find);
	cout << re;
	return 0;
}
 
排序问题
- 二路归并排序
 
两段有序序列
然后谁小把谁拉下来
//Merge
 
- 快速排序
 
划分:选定一个记录作为轴值,以轴值为基准将整个序列划分为两个子序列,把比轴值小的放左边,比轴值大的放右边
左右寻找的方法:最左边放i 最右边放j 然后同时向中间缩,根据大小不断交换
void QuickSort(int a[], int p, int r){
    if(p < r){
        int q = Partition(a, p, r);
        QuickSort(a, p, p - 1);  //对左半段排序
        QuickSort(a, p + 1, r);  //对右半段排序
    }
}
 
int Partition(int a[], int p, int r){
    int i = p, j = r + 1;
    int x = a[p];
    while(ture){
        while(a[++i] < x);
        while(a[--j] > x);
        
    }
}
 
主元选择:
- 随机选(不做要求)
 
 
- 线性时间选择
 
int RandomizedSelect(int a[], int p, int r, int k){
    if(p == r) return a[p];
    int i = RandomizedPartition(a, p, r);
    j = i - p + 1;
    if(k <= j) return RandomizedSelect(a, p, i, k);
    else return RandomizedSelect(a, i + 1, r, k - j);
}
 
划分:n -> 3/4 n
五个一组进行划分 然后每组组内进行排序
1 2 3 4 5
2 3 4 5 6
. . . | . . .
. . . | . . .
. . . | . . .
这样划分后左上角一定比中间的小 右下角一定比中间的大
前面找一个大的 与后面找一个小的地方进行交换
原理:
选择一个基准元素
然后从两端向基准元素探测,直到右边的比基准元素小,左边的比基准元素大,我们进行比较,如果这两个探测的元素还是原位置,没有交界,则这两个元素互换位置。因为我们的目的就是基准元素左边的比它小,右边的比它大
当ij相遇,则交换基准元素位置和相遇位置元素
对左右分割段采用递归调用重复操作
完整代码:
#include <iostream> 
using namespace std;
/**
* 分治法实现快速排序
**/ 
int a[100005];
int n;
 
int Partition(int l, int r){
	int i = l - 1;
	int j = r + 1;
	int x = i + j >> 1; //选择主元      右除 
	while(i < j){
		do{
			i ++;
		}while(a[i] < a[x]);
		do{
			j --;
		}while(a[j] > a[x]);
		if(i < j) swap(a[i], a[j]);
	}
	return j; 
} 
 
void quickSort(int l, int r){
	if(l < r){
		//pos作为分隔元素 对前后进行排练 
		int pos = Partition(l, r);
		quickSort(l, pos);
		quickSort(pos + 1, r); 
	}
}
//数组就从下标为0开始吧 
int main(){
	cin >> n;
	for(int i = 0; i < n; i ++){
		cin >> a[i];
	}	
	quickSort(0, n - 1);
	
	for(int i = 0; i < n; i++){
		cout << a[i] << " ";
	}
	return 0;
}
 
选择问题
选第k小
分治法思想:
一趟划分 分成两个左右子表 主元是随机选的
然后主元+左子表个数为p 如果k=p直接找到 如果k<p 则只在前面找 后面直接舍弃
注意如果k > p时 在右边找的话要找第p-k小
二次选中法确定主元
斯特拉森
0x08 动态规划(Dynamic Programming,DP)*
定义
运筹学的一个分支
与分治法的区别:
-  
子问题与子问题之间有交叉,重叠子问题 只解一次 => 所以效率比较高
 -  
并且这个问题满足最优性原理(问题的最优解包含了子问题的最优解)
 -  
无后向性
 
满足上面三个条件,可用DP
实质就是动态的表格题, 用数组来保存结果, 就是在填dp数组的表
表格是动态的,每次都在修订
大部分是从后往前搞,把原问题转化为子问题,缩小问题规模 即自底向上
硬币组合
0/1 背包问题
特殊点在于物品无法分割 ,只能全放或者全不放,这如果使用贪心算法就可能会存在一些空间的过度浪费
从右边往左边选f(j,X) = max{f(j-1, X), f(j-1, X-wj) + pj}
两步:
dp[i][j] = max{dp[i][j-1], dp[i-wj][j-1] + vj}
 
构造两个二维表格进行存储数据

解释含义:
c 表示当背包容量为c的时候 能放入的最大价值
i 表示只选择i个的物品的情况,注意这里按顺序选择就好 比如i = 2就是计算装前两个的情况
表格数据推导:
当i = 1的时候,只判断第1个物品,c为1到9都装不下 当c=10 也就是背包容量为10的时候,可以装装下,价值为24,当背包容量继续扩大,那包装下的啊
当i = 2的时候,判断第1个和第2个物品,当c=1的时候,针对物品2列式子,有两种可能 针对上一个判断
f(2,1)= f(1, 1-3) + 2 = f(1, -2) + 2 装入
f(2,1) = f(1,1) 不装入
在f函数中第一个参数表示第几个物品,第二个参数表示背包容量情况
因为-2不符合实际要求,所以只能不装入,值为f(1,1)= 0
以此类推,当c=3时
f(2,3)=f(1, 3-3) + 2 = f(1, 0) + 2 = 2 装入
f(2,3)=f(1,3) = 0 不装入
选较大值 即为2
代码实现:
//初始化  n行 m列
for(int i = 0; i <= n; i++) f[i][0] = 0;
for(int i = 0; i <= m; i++) f[0][i] = 0;
for(int i = 1; i <= n; i++){
    for(int j = 1; j <= m; j++){
        //放
        if(j >= w[i]){
            f[i][j] = max(f[i - 1][j - w[i]] + p[i], f[i - 1][j]);
        }
        //不放
        else{
            f[i][j] = f[i - 1][j];
        }
    }
}
printf("%d\n", f[n][m]);
 
例题:



参考:https://cloud.tencent.com/developer/article/2109840
滚动数组
最短路径问题
例题:

从结果往前推,一层一层进行


最长公共子序列(LCS)
实例:
X : A B C B D A B
y : B D C A B A
子序列的定义就是按照下标严格递增
考察末尾字符
如果想要进行切分,条件在于末尾字符要相同,否则去掉末尾字符可能会遗漏一些公共子序列
相等的话则一定是公共子序列啦,所以 是删除后求最优解 然后结果加上末尾相同的个数
构建过程:

回溯代码:
void LCS(int i, int j, X, S){
    if(i == 0 || j == 0) return;
    if(S[i][j] == 1){
        LCS(i - 1, j - 1);
        cout << a[i];
    }
    else if(S[i][j] == 2) LCS(i - 1, j);
    else LSC(i, j - 1);
}
 
时间复杂度 : O(m + n)
例题:


简单来说就是如果行列的字符相同 则是左上角的值+1
如果不相同,则是左边和上面的值比较 取较大值 如果两值相等 直接数据来源于上方
参考:https://blog.csdn.net/2301_79582459/article/details/139198609
最少拦截问题 => LIS 最长递增子序列
hdu1257
举例:
3 1 5 2 4 6
递增序列:
1 2 4 6 (LIS)
1 5 6
3 4 6
1 5
 
以3结尾:dp[0] = 1 数据长度
以1结尾:dp[1] = 1
dp[2] = 2
dp[i] = max{dp[i], dp[j]} + 1 (a[j] < a[i])
- 找出最优解的性质,并刻画其结构特征
 - 递归地定义最优值
 - 以自底向上
 
存模板:
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 100010;
int a[N],q[N];     //a是原数组  q是需要维护的数组存储递增子序列 
int n;
int main()
{
    scanf("%d", &n);
    for (int i = 0; i < n; i ++ ) scanf("%d", &a[i]);
    int len = 0;
    for (int i = 0; i < n; i ++ )
    {
        int l = 0, r = len;
        //二分 不理解?? 
        while (l < r)
        {
            int mid = l + r + 1 >> 1;
            if (q[mid] < a[i]) l = mid;
            else r = mid - 1;
    	}
        len = max(len, r + 1);
        q[r + 1] = a[i];    //不断给q插值 
    }
    printf("%d\n", len);
    return 0;
}
 
矩阵连乘问题
每行 和 每列 连乘相加 放到元素位置
乘法次数 10 * 100 100 * 5 = > 10 * 100 * 5 !是这样的
在位置k处切一道 左边最小 + 右边最小
画表 记录 在数组中
两个矩阵为4,6 6,10
次数为4 * 6 * 10
一般设置两个表
一个存放最优解的值 另一个存放如何切分才能得到最优解
设置数组
对于一系列矩阵
  
      
       
        
         
         
           A 
          
         
           i 
          
         
         
         
           A 
          
          
          
            i 
           
          
            + 
           
          
            1 
           
          
         
        
          . 
         
        
          . 
         
        
          . 
         
         
         
           A 
          
         
           j 
          
         
         
        
          m 
         
        
          [ 
         
        
          i 
         
        
          ] 
         
        
          [ 
         
        
          j 
         
        
          ] 
         
        
          表示 
         
         
         
           A 
          
         
           i 
          
         
        
          到 
         
         
         
           A 
          
         
           j 
          
         
        
          的最小乘法次数 
         
        
       
         A_iA_{i+1}...A_j\\ m[i][j]表示A_i到A_j的最小乘法次数 
        
       
     AiAi+1...Ajm[i][j]表示Ai到Aj的最小乘法次数


对于n个矩阵 使用q这个一维数组 n+1个元素就够了
例题:


0x09 基础语法
引用:
int a = 5;
int &x = a;   //x作为a的一个引用
x = 4;
printf("%d", a);  //输出是4
 
0x10 二分答案
概念
使用二分查找需要满足:一定要有序 单调性
识别题目:存在最大的最少 最少的最大这类字眼
#include <cstdio>
int a[1000000], n;  //全局变量自动赋值为0
//如果是局部变量 记得赋值0 然后使用sum++ 这类函数 否则是随机数进行++
/**
 * 基本二分查找实现
*/
int check(int key){
    int left = 0, right = n - 1, mid;
    while(left <= right){   //一定是小于等于
        mid = (left + right) >> 1;   //这就是除2  其实右移和除2不是一回事 因为在正数情况下无影响 负数存在向0取整-3 右移是-2  除2是-1
        if(key == a[mid]) return 1;
        if(key > a[mid]){
            left = mid + 1;
        }
        else if(key < a[mid]){
            right = mid - 1;
        }
    }
    return 0;   //在win下不写也能过 但是在Linux中有问题
}
 
例题:洛谷 P1873
看完题目一定要看一下题目范围 初步判断一下这个题目是否需要开long long
#include <bits/stdc++.h>
using namespace std;
/**
 * 定义变量:
 * 1. 尽量全局
 * 2. 尽量见词知意
 * 
*/
int n, m, l, r = 1e9, mid;    //1e9=10的9次方
long long a[1000005], sum;
int main(){
    scanf("%d%d", &n, &m);
    for(int i = 1; i <= n; i++){
        scanf("%lld", &a[i]);
    }
    while(l <= r){ //二分结果:1. rl位置关系 2. l = r + 1 
        sum = 0;  //清零
        mid = (l + r) / 2;
        for(int i = 1; i <= n; i ++){
            if(a[i] > mid){
                sum += a[i] - mid;
            }
        }
        //可以写sum >= m  然后可以删除掉最后else的部分 因为相等时l定住不动 r一点点靠近
        if(sum > m){
            l = mid + 1;
        }
        else if(sum < m){
            r = mid - 1;
        }
        //仅仅这个题中符合该条件
        else{
            cout << mid;
            return 0;
        }
    }
    cout << r;   //需要分析清楚
    return 0;
}
 
P1182 数列分段 SectionII
P1824 进击的奶牛
//P1824 进击的奶牛
#include<iostream>
#include<cstdio>
#include<algorithm>
using namespace std;
int n,c,a[100005];
int check(int mid){
	int pos=1;
	int k=c;
	while(--k){
		pos=lower_bound(a+1, a+n+1, a[pos]+mid)-a;
		if(pos==n+1) {
			return 0;
		}
	}
	return 1;	
}
int main(){
	scanf("%d%d",&n,&c);
	for(int i=1; i<=n; i++) scanf("%d",&a[i]);
	sort(a+1, a+n+1);
	int l=1,r=1e9;
	while(l<=r){
		int mid=(l+r)>>1;
		if(check(mid)) l=mid+1;
		else r=mid-1;		
	}
	cout<<r;
	return 0;
}
/*
二分答案
这里二分的是奶牛之间的间隔 
*/
 
练习
课后oj例题:
A:奶茶店选址
B:野外探险
分析:
D天活动,N份干粮,N份粮食进行编号,小明从小到大开始吃 花费D天全部吃完
每份粮食能量值为Hi 每天获得能量是当天吃下的所有干粮的能量值之和 (每天吃或者不吃 吃多少都不一定)
每天的能量会在第二天开始时消耗掉一半 注意是整除的一半
设计方案,让小明获得能量值最小的那一天最大 ! 关键字眼(最小的最大)
思考:
对答案进行二分
答案的最小值是1 最大一定要设置的大一点 预测一下答案范围
解决:
每一天都要大于mid 所以不断吃 直到大于mid换下一天 看看能否正好满足天数
代码:
#include<bits/stdc++.h>
using namespace std;
int N, D;
int a[50005];
int check(int mid){
	int k = 1;  //表示第几份食物 
	int sum[D + 5];
	//该函数的作用是给sum赋值为0  N个食物  D天 
	memset(sum, 0, sizeof(sum));
	//第1天灌食   因为mid是最小 所以每天获得的大于mid才能到下一天 
	while(sum[1] < mid && k <= N){
		sum[1] += a[k];   //吃能量 
		k++;
	} 
	if(sum[1] < mid){
		return 0;
	}    
	//第2天往后 
	for(int i = 2; i <= D; i++){
		sum[i] = sum[i - 1] / 2;      //获取前一天的能量值 
		while(sum[i] < mid && k <= N){    //注意啊 N份食物 可以一直吃 所以有等号 要不然结果不对! 
			sum[i] += a[k];
			k++; 
		} 
		if(sum[i] < mid){
			return 0;
		}      
	} 
	return 1;
}
int main(){
	scanf("%d%d", &N, &D); 
	for(int i = 1; i <= N ; i++){
		scanf("%d", &a[i]); 
	}
	//需要思考 二分的是答案 对答案进行二分查找 作为最后的结果不断带进场景中做check 
	int l = 1, r = 1e9; 
	while(l <= r){
		int mid = (l + r) >> 1;
		if(check(mid)){
			l = mid + 1;
		} 
		else r = mid - 1;
	} 
	cout << r;
	return 0;
} 
 
C:龙年邮票
分析:集邮票活动,一套完整的龙年邮票共有n种不同的造型
小明有第i种造型的邮票ai张,m张万能邮票
万能邮票可以抵任意一张,问一共能集齐多少套邮票
非常像之前支付宝的集五福啊!还有一个万能福
思考:
同样是对答案进行二分查找
代码:(有点想不到)
#include <bits/stdc++.h>
using namespace std;
int n, m;
int a[100]; 
int b[100];
int check(int mid){
	int sum=0;     //记录万能票使用数量 
    memcpy(b,a,sizeof b); //把a的值赋给b  对b进行操作 
    for(int i = 1;i <= n; i++)
    {
        int x = mid - a[i];   //套数 - 当前品种数量    1 2 3   3       0 1
		//如果套数小于当前品种邮票数量 正常执行 检测下一个品种的邮票 
        if(x <= 0)
          continue;
        //如果当前品种邮票不足以支撑目标套数 则启动万能票  差x张     2        3
        else
        {
            for(int j = 1; j <= n; j++)
            {
                if(i != j && b[j] < x) //如果存在其他品种 比当前差值还少 直接完蛋 因为万能票只能用一次 
                  return 0;
                else if(i != j)     //否则 对其他品种减去万能票数 
                   b[j] -= x;
            }
        }
        sum += x;
    }
    if(sum <= m)
      return 1;
    return 0;
	
	
}
int main(){
	cin >> n >> m;
	for(int i = 1; i <= n; i++){
		cin >> a[i];
	}
	//把所有存在的邮票 按数量从小到大排列 
	sort(a + 1, a + 1 + n);
	
	int l = 1, r = 1e9;
	while(l <= r){
		int mid = (l + r) >> 1;
		if(check(mid)){
			l = mid + 1;
		}
		else{
			r = mid - 1;
		}
	}
	cout << r;
	return 0;
} 
 
D : 消耗的能量
分析:n行 m列数字矩阵 第一行均为0 从第一行出发 到第n行任意位置结束
规定消耗能量为每行行走过的格子各自和的最大值
求所有路径中,小明消耗能量的最小是多少
思考:最大 最小!!! 二分答案的关键词!!!
然后涉及到路径 那么我们考虑使用bfs 去探测路径
然后在探测路径的过程中,需要去对每一行的横向进行能量探测,如果能量大于我们的二分答案值 则换下一个答案
#include <bits/stdc++.h>
using namespace std;
int n, m;
int a[1005][1005];
/*
  来存一个BFS的模板 
*/ 
int dx[] = {1, 0, -1, 0};
int dy[] = {0, 1, 0, -1};
int vis[1005][1005];   //做标记 是否访问过 
struct P{
    int x,y,z;
};
bool BFS(int x, int y, int mid){
	queue<P> q;
	q.push({x, y, a[x][y]});
	while(!q.empty()){
		P x = q.front();
		q.pop();
		//
		if(a[x.x][x.y] <= mid){
			//如果走到头了   直接返回true 
			if(x.x == n) return true;
			//没走到头 继续往下走
			for(int k = 0; k < 4; k++){
				int nx = x.x + dx[k];
				int ny = x.y + dy[k];
				//矩阵范围判断 
				if(nx >= 1 && ny >= 1 && nx <= n && ny <= m && vis[nx][ny] == 0 && a[nx][ny] <= mid){
					q.push({nx, ny, max(x.z, a[nx][ny])});
					vis[nx][ny] = 1; //表示访问过了 
				}
			} 
			 
		} 
	}
    return false; 
}
int check(int mid){ 
	memset(vis, 0, sizeof vis); //对vis数组 全部赋值 为0  表示没有访问过
	bool ans = false;
	for(int i = 1; i <= m; i++){
		ans = ans || BFS(2, i, mid);  //第1行是初始行 全部为0 所以从第2行开始BFS 
	} 
	return ans;
}
int main(){
	cin >> n >> m;
	for(int i = 1; i <= n; i++){
		for(int j = 1; j <= m; j++){
			cin >> a[i][j];
		}
	}
	int l = 1, r = 1005;
	while(l <= r){
		int mid = (l + r) >> 1;
		if(!check(mid)){    //思考一下返回的是什么 不要光想着套模板!! 
			l = mid + 1;
		}
		else{
			r = mid - 1;
		}
	}
	cout << l;
	return 0;
}
 
整数域二分
洛谷 P1577 切绳子
有一个问题就是如果答案结果是1.99
与2.00进行相减判断 但是不通过 因为二进制表示的问题,0.1无法用二进制精确表示,所以在二进制相减中可能存在问题
Specual Judge问题,不用太在意格式 因为最终的评判标准时是与标准值作差 然后进行评判的 一般说保留三位 但是要考虑精度问题 尽量保留5位
转换方法:将小数转化到整数 先扩大 后结果缩小
实数域二分
P1570 KC 喝咖啡
P1678 烦恼的高考志愿
0x11 贪心法(Greedy)*
导言
找零问题:
每一步的判断是当前的最优选择,但是需要思考局部最优能否达到全局最优的问题
局部最优代替整体最优,其最终结果近似最优解
有点像背包问题啊
给个例子:
xi = {1, 5, 10, 20, 50, 100}
y = ∑ w i x i \sum w_ix_i ∑wixi
求min ∑ w i \sum w_i ∑wi
可行解:问题给定一些约束条件,可以满足
最优解:
通过分步决策的方法,每一步决策产生一个向量
流程:
- 存在可能解的集合
 
背包问题
0/1 背包问题
物体不能分割 只有两种状态 装入或者不装
一般背包问题
物体可分割,可以把物体x的xi部分装入到背包中
贪心思路:
- 价值最大
 - 重量最轻
 - 单位重量价值最大
 
方案一:按物体价值降序装包
方案二:按物品重量非降序装包
方案三:按物体价值与重量比值的降序装包 先装最大的
活动安排问题
按结束的时间进行排列
最佳合并模式
类似于哈夫曼树
0x12 数论
质数
快速幂
( a b ) m o d n (a^b) ~mod~ n (ab) mod n
快速幂就是解决上述问题
算法的思想是:分治
区分一下分治和二分的区别:
二分是舍弃掉一半,而分治是不断划分 然后合而治之
模板:
递归代码:
int quickPow(int a, int b){
    if(b == 1) return a;
    if(b % 2 == 0){
        int t = quickPow(a, b/2);
        return t * t % mod;
    }
    else{
        int t = quickPow(a, b/2);
        t = t * t % mod;
        t = t * a % mod;
        return t;
    }
}
 
非递归代码1:
int quickPow(int a, int b){
    int t = 
}
 
补充:
while(b){
    b = b / 2;
}
 
该式的时间复杂度为log n 级别的
例题:P1226
例题2:
数列求值
矩阵快速幂
单位矩阵:主对角线上的元素都是1
void matrixMulti(){
    for(int i = 1; i <= m; i++){
        for(int)
    }
}
 
最关键的地方:
不是矩阵乘幂的计算,而是如何得到一个可以用来形成乘幂计算的矩阵
S n = 1 ∗ S n − 1 + 1 ∗ f n + 0 ∗ f n − 1 f n + 1 = 0 ∗ S n − 1 + 1 ∗ f n + 1 ∗ f n − 1 f n = 0 ∗ S n − 1 + 1 ∗ f n + 0 ∗ f n − 1 S_n=1*S_{n-1}+1*f_n+0*f_{n-1}\\ f_{n+1}=0*S_{n-1}+1*f_n+1*f_{n-1}\\ f_n=0*S_{n-1}+1*f_n+0*f_{n-1} Sn=1∗Sn−1+1∗fn+0∗fn−1fn+1=0∗Sn−1+1∗fn+1∗fn−1fn=0∗Sn−1+1∗fn+0∗fn−1
约数
求N的正约数集合:
- 试除法
 - 倍数法
 
最大公约数和最小公倍数 P1029
扩欧算法
贝祖定理(斐蜀定理)
0x13 图论
基本概念
无向图和有向图的连通性
割点:去掉这个点之后 剩下的图不连通 那么这个点就是割点
割边:
双连通问题:
0x14 回溯法*
基本概念
需要找出问题的最优解 在一定的约束下进行,使用回溯法
设置一个目标函数,用来衡量每一个可能解的情况
搜索:
- 以深度优先方式搜索解空间
 - 在搜索过程中使用剪枝函数,避免无效搜索
 常用剪枝函数: 舍得!不行的就放弃 别挣扎
- 用约束函数在扩展结点处减去不满足约束的子树 (不满足不可行)
 - 用限界函数剪去得不到最优解的字数 (可行但不最优)
 
涂色问题
前提:
- 不是优化问题,没有限界函数
 - 搜索策略:深度优先
 - 约束条件:相邻结点,不同颜色
 
4/n-皇后问题
解向量 :
  
      
       
        
        
          ( 
         
         
         
           x 
          
         
           1 
          
         
        
          , 
         
         
         
           x 
          
         
           2 
          
         
        
          , 
         
        
          . 
         
        
          . 
         
        
          . 
         
        
          , 
         
         
         
           x 
          
         
           n 
          
         
        
          ) 
         
        
       
         (x_1,x_2,...,x_n) 
        
       
     (x1,x2,...,xn)
 注意这个写法 直接限制了每一行只能放一个
设置约束条件:
限制列:i != j
限制对角线:
程序:
bool Place(int k, int i, int* ){
    for()
}
 
0/1背包问题
0x15 分支限界法*
4-皇后问题
广度优先
//结构体模板
template <class T>
struct Node{
    T cost;
    Node* parent;
}
Node<int> t;  //创建结构体 意味着上面模板中的T为int
 
注意上下界问题

















