本文原载自我的个人博客
1.第一章 基础算法
时间复杂度
重视思想
好的方法 课下 模板(重复3-5次)思想理解会背 然后做题目
根据数据量猜解法
常数指令 一次a+b ,i ++这种就是,假如给你一个1e6长度的数组,用n^2的算法的话,数据量就成了1e12,我们设计的算法肯定不能超过1e7~1e8,假如用n*log2 n ,算下来也就是1e7多一点,未必能超,也许能行。假如是1e3长度,那么n2也行。
注意:上面使用技巧的必要条件↑
快速排序
用这个就行,防止边界问题
归并排序
思想也是分治,但是方法不一样
先划分区间,再合并
最后那个for是把temp拷贝回去 temp是记录递归排序的数组,每一步都在改变,不能最后直接输出temp
C++
#include<iostream> using namespace std; int a[100010]; int t[100010]; /*void merge_sort(int a[],int l,int r){ if(l == r)return; int mid = l + r >> 1; merge_sort(a,l,mid),merge_sort(a,mid+1,r); for(int i = l,j = l, k = mid + 1; i <= r; i ++){ if(j == mid + 1) t[i] = a[k ++]; else if(k == r + 1) t[i] = a[j ++]; else t[i] = a[j] < a[k] ? a[j ++] : a[k ++]; } for(int i = l; i <= r; i ++)a[i]=t[i]; }*/ void merge_sort(int a[], int l, int r){ //只剩一个元素的时候,已经有序,返回 if(l >= r) return; //寻找数组中点下标 int mid = (l+r)>>1; //递归给左半边排序 merge_sort(a,l,mid); //递归给右半边排序 merge_sort(a,mid+1,r); //以下是合并排序好的两个数组 //k:遍历合并后的数组的下标 int k = 0; //i:左半边数组的下标,j:右半边数组的下标 int i = l, j = mid + 1; //左右半边都没遍历完 while(i <= mid && j <= r){ //左边的元素小于右边的元素 if(a[i] < a[j]) //左边元素放如临时数组,并移动下标 temp[k++] = a[i++]; //否则,右边元素放入临时数组并移动下标 else temp[k++] = a[j++]; } //如果左边数组有剩余,则放入临时数组 while(i <= mid) temp[k++] = a[i++]; //如果有边数组有剩余,则放入临时数组 while(j <= r) temp[k++] = a[j++]; //把临时数组中的元素拷贝至原数组 k = 0; for(int i = l; i <= r; i++){ a[i] = temp[k++]; } } int main(){ int n; scanf("%d",&n); for(int i = 0 ; i < n; i ++){ scanf("%d",&a[i]); } merge_sort(a,0,n-1); for(int i = 0 ; i < n; i ++){ printf("%d ",a[i]); } } |
例题 逆序对
https://www.acwing.com/solution/content/5103/讲解很好
分治
计数排序
桶排序
例题洛谷P7020,适用情况,要排序的数字值域小,但是n很大的情况,sort和快排这些适合值域大小都可以,n小点的情况
二分
C++
没有单调性也可能二分(本质不是单调性) 整数二分算法模板 —— 模板题 AcWing 789. 数的范围 bool check(int x) {/* ... */} // 检查x是否满足某种性质 // 区间[l, r]被划分成[l, mid]和[mid + 1, r]时使用: int bsearch_1(int l, int r) { while (l < r) { int mid = l + r >> 1; if (check(mid)) r = mid; // check()判断mid是否满足性质 else l = mid + 1; } return l; } // 区间[l, r]被划分成[l, mid - 1]和[mid, r]时使用: int bsearch_2(int l, int r) { while (l < r) { int mid = l + r + 1 >> 1; if (check(mid)) l = mid; else r = mid - 1; } return l; } 浮点数二分算法模板 —— 模板题 AcWing 790. 数的三次方根 bool check(double x) {/* ... */} // 检查x是否满足某种性质 double bsearch_3(double l, double r) { const double eps = 1e-6; // eps 表示精度,取决于题目对精度的要求 while (r - l > eps) { double mid = (l + r) / 2; if (check(mid)) r = mid; else l = mid; } return l; } |
想清楚性质 边界
无解和二分无关,可以用二分出的结果判断原题有没有解
浮点数二分更简单,l和r距离足够小就可以认为是个数了
而且不用考虑向上向下取整 直接/2
浮点数二分算法模板 —— 模板题 AcWing 790. 数的三次方根
C++
bool check(double x) {/* ... */} // 检查x是否满足某种性质 double bsearch_3(double l, double r) { const double eps = 1e-6; // eps 表示精度,取决于题目对精度的要求 while (r - l > eps) { double mid = (l + r) / 2; if (check(mid)) r = mid; else l = mid; } return l; } |
有经验,就是假如题目要求4位的话,最好-6,就是比要求的多两位,保证没问题
例题
二分答案(10.15-10.17 11.13)
关于lower_bound( )和upper_bound( )的常见用法
高精度(略过)
前缀和 和 差分
一维前缀和 —— 模板题 AcWing 795. 前缀和S[i] = a[1] + a[2] + ... a[i]
a[l] + ... + a[r] = S[r] - S[l - 1]
二维前缀和 —— 模板题 AcWing 796. 子矩阵的和
S[i, j] = 第i行j列格子左上部分所有元素的和
以(x1, y1)为左上角,(x2, y2)为右下角的子矩阵的和为:
S[x2, y2] - S[x1 - 1, y2] - S[x2, y1 - 1] + S[x1 - 1, y1 - 1]
一维差分 —— 模板题 AcWing 797. 差分
给区间[l, r]中的每个数加上c:B[l] += c, B[r + 1] -= c
先得到差分数组
二维差分 —— 模板题 AcWing 798. 差分矩阵
给以(x1, y1)为左上角,(x2, y2)为右下角的子矩阵中的所有元素加上c:
S[x1, y1] += c, S[x2 + 1, y1] -= c, S[x1, y2 + 1] -= c, S[x2 + 1, y2 + 1] += c
这个讲解很好AcWing 798. 差分矩阵 【 c++详细题解 】 - AcWing
位运算(待更)
离散化
如果数据范围小的话(10的五次方以内)用前缀和也可
C++用vector作离散化
下面是去重函数的用法
去重完用sort排序最后用二分查找(好像是先排序后去重
二分函数lower_bound(大于等于) upper_bound(大于)
lower_bound()/upper_bound()函数(C++)_c++ lowerbound upperbound-CSDN博客
https://www.acwing.com/solution/content/80100/这个代码解释非常详细
csdn的这个博客也很不错(11.11)
区间合并
好多端点问题都是贪心
https://www.acwing.com/solution/content/2615/更新维护区间(很重要)
2.第二章 数据结构
链表和邻接表
单链表
用的最多的是邻接表
(存储图和树)
双链表
添加的顺序不能反
邻接表
栈和队列
表达式求值(中缀 后缀)一般是二元(不包括符号)
单调栈(2023.12.15)
例题洛谷P2866
C++
#include<bits/stdc++.h> using namespace std; stack<int> st; int n; int main(){ cin >> n; long long ans= 0; long long cnt = 0; for(int i = 1 ; i <=n ; i ++){ int x; cin >> x; while(!st.empty()&&st.top()<=x){ st.pop(); cnt--; } ans+=cnt; st.push(x); cnt++; } } cout << ans; return 0; } |
单调队列
用STL的双端队列deque非常方便
例题 洛谷P2032
C++
#include<bits/stdc++.h> using namespace std; const int N = 2e6 + 10; struct node{int v,id;}a[N]; deque<node> q; int n,k; int main(){ cin >> n >>k; for(int i= 1; i <= n; i ++){ cin >> a[i].v; a[i].id = i; } for(int i = 1; i <= n; i ++){ while(!q.empty()&&q.back().v<a[i].v){ q.pop_back(); } q.push_back(a[i]); if(q.front().id==i-k)q.pop_front(); if(i>=k)cout << q.front().v<<endl; } return 0; } |
虽然抽象但是题型固定就那几个
AcWing 830. 单调栈--图解,详细注释 - AcWing 这个非常易于适合理解单调栈
单调栈:见博客
kmp算法(略)
Trie
题一般字符串字母类型少
算法竞赛里的trie树最多26个或者52个字母
并查集(12.8)
https://leetcode.cn/circle/discuss/qmjuMW/
堆(12.19)
AcWing 838. 堆排序--海绵宝宝来喽 - AcWing
堆排序:
从最后一个非叶子节点开始
向上调整
向下调整
哈希表
字符串哈希适用场景:不考虑冲突,不能映射成0
字符串前缀哈希值
树状数组
例题 动态求区间和
树状数组
CPP
#include<bits/stdc++.h> using namespace std; const int N=100009; int a[N],tr[N]; int n,m; //每个数的间隔,背下来就行 int lowbit(int x) { return x&-x; } //第x个数加上v int add(int x,int v) { //因为树状数组的性质,加一个数,只影响logn个数,所有不用全加完 //从当前位置开始加,每个间隔是lowbit(i),一直加到最后 for(int i=x;i<=n;i+=lowbit(i)) tr[i]+=v; } //返回x的前缀和 int qurry(int x) { //因为树状数组的性质,求前缀和,只用加logn个数,所有不用全加完 //从当前位置开始累加,每个间隔是lowbit(i),一直加到i==0停止 int cnt=0; for(int i=x;i!=0;i-=lowbit(i)){ cnt+=tr[i]; } return cnt; } int main() { cin>>n>>m; for(int i=1;i<=n;i++) scanf("%d",&a[i]); for(int i=1;i<=n;i++) add(i,a[i]);//第i个数加上a[i] while(m--) { int k,x,y; scanf("%d%d%d",&k,&x,&y); if(k==0) printf("%d\n",qurry(y)-qurry(x-1)); else add(x,y); } return 0; } |
这道题的线段树方法
4倍空间的原因
CPP
#include<iostream> #include<cstring> #include<cstdio> #include<algorithm> using namespace std; const int N=100010; int n,m; int w[N];//记录一下权重 struct node{ int l,r;//左右区间 int sum;//总和 }tr[N*4];//记得开 4 倍空间 void push_up(int u)//利用它的两个儿子来算一下它的当前节点信息 { tr[u].sum=tr[u<<1].sum+tr[u<<1|1].sum;//左儿子 u<<1 ,右儿子 u<<1|1 } void build(int u,int l,int r)/*第一个参数,当前节点编号,第二个参数,左边界,第三个参数,右边界*/ { if(l==r)tr[u]={l,r,w[r]};//如果当前已经是叶节点了,那我们就直接赋值就可以了 else//否则的话,说明当前区间长度至少是 2 对吧,那么我们需要把当前区间分为左右两个区间,那先要找边界点 { tr[u]={l,r};//这里记得赋值一下左右边界的初值 int mid=l+r>>1;//边界的话直接去计算一下 l + r 的下取整 build(u<<1,l,mid);//先递归一下左儿子 build(u<<1|1,mid+1,r);//然后递归一下右儿子 push_up(u);//做完两个儿子之后的话呢 push_up 一遍u 啊,更新一下当前节点信息 } } int query(int u,int l,int r)//查询的过程是从根结点开始往下找对应的一个区间 { if(l<=tr[u].l&&tr[u].r<=r)return tr[u].sum;//如果当前区间已经完全被包含了,那么我们直接返回它的值就可以了 //否则的话我们需要去递归来算 int mid=tr[u].l+tr[u].r>>1;//计算一下我们 当前 区间的中点是多少 //先判断一下和左边有没有交集 int sum=0;//用 sum 来表示一下我们的总和 if(mid>=l)sum+=query(u<<1,l,r);//看一下我们当前区间的中点和左边有没有交集 if(r>=mid+1)//看一下我们当前区间的中点和右边有没有交集 sum+=query(u<<1|1,l,r); return sum; } void modify(int u,int x,int v)//第一个参数也就是当前节点的编号,第二个参数是要修改的位置,第三个参数是要修改的值 { if(tr[u].l==tr[u].r)tr[u].sum+=v; //如果当前已经是叶节点了,那我们就直接让他的总和加上 v 就可以了 //否则 else { int mid=tr[u].l+tr[u].r>>1; //看一下 x 是在左半边还是在右半边 if(x<=mid)modify(u<<1,x,v);//如果是在左半边,那就找左儿子 else modify(u<<1|1,x,v);//如果在右半边,那就找右儿子 //更新完之后当前节点的信息就要发生变化对吧,那么我们就需要 pushup 一遍 push_up(u); } } int main() { scanf("%d%d",&n,&m); for(int i=1;i<=n;i++)scanf("%d",&w[i]); build(1,1,n);/*第一个参数是根节点的下标,根节点是一号点,然后初始区间是 1 到 n */ //后面的话就是一些修改操作了 while(m--) { int k,a,b; scanf("%d%d%d",&k,&a,&b); if(!k)printf("%d\n",query(1,a,b));//求和的时候,也是传三个参数,第一个的话是根节点的编号 ,第二个的话是我们查询的区间 //第一个参数也就是当前节点的编号 else modify(1,a,b);//第一个参数是根节点的下标,第二个参数是要修改的位置,第三个参数是要修改的值 } return 0; } 作者:Elegant 链接:https://www.acwing.com/solution/content/40394/ 来源:AcWing 著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 |
区间求和例题:洛谷P3368
CPP
#include<bits/stdc++.h> using namespace std; const int N = 5e5 + 10; int a[N]; int b[N]; int tr[N]; int n,m; int lowbit(int x){ return x & -x; } void add(int k,int c){ for(int i = k; i <= n; i += lowbit(i)){ tr[i] += c; } } int query(int x){ int res = 0; for(int i = x; i > 0; i -= lowbit(i)){ res += tr[i]; } return res; } int main(){ cin >> n >> m; for(int i = 1; i <= n; i ++){ cin >> a[i]; b[i] = a[i] - a[i - 1]; add(i,b[i]); } while(m --){ int op; cin >> op; if(op == 1){ int x,y,k; cin >> x >> y >> k; add(x,k); add(y + 1,-k); }else{ int x; cin >> x; cout << query(x) << endl; } } } |
线段树(待更)
3.第三章 搜索与图论
深度优先搜索(DFS)
两个很重要的概念:回溯 剪枝
每个DFS都一定对应一个搜索树
AcWing 842. 排列数字--深度优先遍历代码+注释 - AcWing
** 经典题目**
八皇后
广度优先搜索(BFS)
不是所有最短路都用BFS,只能当边权都是1时,一般情况都是用最短路算法
BFS函数里一般都用队列
树与图的存储(10.21)
【AgOHの数据结构】你真的了解链式前向星吗?_哔哩哔哩_bilibili
这个链式前向星讲的特别好↑
树是特殊的图,所以只讲图就行
图:有向图,无向图,边有无方向,无向图可以看作特殊的有向图,所以只要看有向图
有向图(a->b):
邻接矩阵g[a] [b] a->b,有重边保留一条就可以了
邻接表
树与图的深度优先遍历
**https://leetcode.cn/circle/discuss/FyPTTM/**宝藏
讲解了为什么要反向建边 [洛谷日报第85期]图论的小技巧以及扩展_网易订阅
洛谷这个题↑正向遍历超时了 n 1e5 ,用反向遍历更适合
思路真妙,把大的先走完,打上标记,肯定就是最大的了
图的讲解(2023.12.4很好) 登录 - Luogu Spilopelia
这个邻接表写法nice↓
CPP
#include <iostream> #include <vector> #include <queue> #include <cstring> #include <algorithm> using namespace std; const int MAXN = 100010; std::vector<int> G[MAXN]; int n, m; bool visited[MAXN]; queue<int> q; void dfs(int x, int cur) {//x指当前所在的节点,cur指已遍历过的节点个数 visited[x] = true;//标记以避免重复访问 cout << x << " ";//输出 if (cur == n) return ; for (int i=0; i<G[x].size(); i++) if (!visited[G[x][i]]) dfs(G[x][i], cur+1);//记得要判断是否遍历过 } void bfs(int x) { memset(visited, false, sizeof(visited));//记得一定要清空 visited[x] = true; q.push(x); while (!q.empty()) { int v = q.front(); q.pop();//记得要弹出,否则会一直在第一层遍历 cout << v << " ";//输出 for (int i=0; i<G[v].size(); i++) if (!visited[G[v][i]]) { visited[G[v][i]] = true; q.push(G[v][i]);//记得要入队 } } } int main() { cin >> n >> m; for (int i=1; i<=m; i++) { int u, v; cin >> u >> v; G[u].push_back(v);//标准邻接表建有向图 } for (int i=1; i<=n; i++) sort(G[i].begin(), G[i].end());//标准vector排序 dfs(1, 0); cout << endl; bfs(1); cout << endl; return 0;//完结撒花! } |
非常重要!!!由前+中/后+中构建序列 模板(字符串切割 + 练习STL substr)
C++
由中序和前序求后序 #include<bits/stdc++.h> using namespace std; string pre,inor; void dfs(string pre,string inor){ if(pre.empty())return; char root = pre[0]; int k = inor.find(root); pre.erase(pre.begin()); string leftpre = pre.substr(0,k); string rightpre = pre.substr(k); string leftinor = inor.substr(0,k); string rightinor = inor.substr(k + 1); dfs(leftpre,leftinor); dfs(rightpre,rightinor); printf("%c",root); } int main(){ cin >> inor >>pre ; dfs(pre,inor); putchar('\n'); return 0; } |
C++
由中序和后序求前序 #include<bits/stdc++.h> using namespace std; string inor,post; void solve(string inor,string post){ if(post.empty())return; char root = post[post.size() - 1]; post.erase(post.end()-1); int k = inor.find(root); string inorleft = inor.substr(0,k); string inorright= inor.substr(k + 1); string postleft = post.substr(0,k); string postright = post.substr(k); cout << root; solve(inorleft,postleft); solve(inorright,postright); } int main(){ cin >> inor >> post; solve(inor,post); return 0; } |
CPP
#include<string> #include<cstring> #include<iostream> #include<cstdio> using namespace std; string pre,inor; void work(string pre,string inor) { if(pre.empty())return; //如果序列空了,就没必要继续了 char root=pre[0]; //取到前序序列的首字母,即根节点 int k=inor.find(root); //找到中序序列中根节点的位置 pre.erase(pre.begin()); //删去前序序列中的根节点 string leftpre=pre.substr(0,k); //从0开始切割k个 string rightpre=pre.substr(k); //从k开始切割到最后 string leftinor=inor.substr(0,k); //从0开始切割k个 string rightinor=inor.substr(k+1); //从k+1开始切割到最后 work(leftpre,leftinor); work(rightpre,rightinor); printf("%c",root); //因为要输出后序序列,所以是左右根 //先遍历左子树,再右子树,再根节点 } int main() { cin>>inor>>pre; work(pre,inor); putchar('\n'); return 0; } |
树与图的宽度优先遍历(待更)
DAG() 与 拓扑排序(12.4)
知乎的讲解,很好↓[知识点]
拓扑排序邻接表版[代码]
C++
#include<bits/stdc++.h> using namespace std; const int N = 1e5 + 10; vector<int> G[N];//邻接表 queue<int> q;//队列操作 int d[N];//统计入度 int n,m,cnt,ans[N];//ans数组记录答案 int main(){ cin >> n >> m; for(int i = 1 ;i <= m; i ++){ int x,y; cin >> x>> y; G[x].push_back(y); d[y]++;//统计入度 } for(int i =1; i <= n; i ++){ if(d[i]==0)q.push(i);//1 } while(q.size()){ int t = q.front(); q.pop(); ans[cnt++] = t; for(int i = 0 ; i < G[t].size();i ++){ d[G[t][i]]--;//删边操作 if(d[G[t][i]]==0) q.push(G[t][i]);//删完后入度为0的话,放入队列 } } if(cnt == n)for(int i = 0; i <cnt; i ++)cout << ans[i]<<" "; else cout <<-1; return 0; } |
例题
我发现下面两道需要拓扑的题,只需要把一些求ans的代码放到模板里,模板还是不变的
1 .洛谷P1113 杂务
C++
/** 每个任务前的耗时最长的任务相加即为答案,详情看洛谷深入浅出 **/ #include <iostream> #include <vector> #include <queue> using namespace std; const int N = 500005; int ind[N], f[N], a[N]; // ind--入度 f--答案 a--时间 vector<int> edge[N]; queue<int> q; int main() { int n; cin >> n; for (int i = 1; i <= n; i++) { int x; cin >> x; cin >> a[i]; while (true) { int y; cin >> y; if (y == 0) break; edge[y].push_back(x); ind[x]++; } } // 步骤一 for (int i = 1; i <= n; i++) { if (ind[i] == 0) { q.push(i); f[i] = a[i]; } }; while (!q.empty()) { int rhs = q.front(); q.pop(); // 步骤二 for (int i = 0; i < edge[rhs].size(); i++) { int u = edge[rhs][i]; ind[u]--; if (ind[u] == 0) q.push(u); // 步骤三 f[u] = max(f[u], f[rhs] + a[u]); } } int ans = 0; for (int i = 1; i <= n; i++) { ans = max(ans, f[i]); // 统计答案 } cout << ans << endl; return 0; } |
2.P4017 最大食物链计数
C++
/** 先找出所有入度为0的,初始链数为1,后面搜到的每个点,都等于各个点链数之和,详情看洛谷深入浅出 **/ #include<bits/stdc++.h> using namespace std; const int N = 5e3 + 10; int ans[N],outd[N],ind[N]; vector<int>G[N]; queue<int>q; int main(){ int n,m; cin >> n >> m; for(int i = 0; i < m; i ++){ int x,y; cin >> x >> y; ind[y]++; outd[x]++; G[x].push_back(y); } for(int i = 1; i <= n; i ++){ if(ind[i]==0){ q.push(i); ans[i] = 1; } } while(q.size()){ auto t = q.front(); q.pop(); for(int i = 0 ; i < G[t].size(); i ++){ int x = G[t][i]; ind[x] --; if(ind[x]==0){ q.push(x); } ans[x] = (ans[x] + ans[t])%80112002; } } int res = 0; for(int i = 1; i <= n; i ++){ if(outd[i]==0){ res= (res + ans[i])%80112002; } } cout << res; return 0; } |
最短路径
力扣总结精华
https://leetcode.cn/circle/discuss/FyPTTM/#%E6%9C%80%E7%9F%AD%E8%B7%AF%E5%BE%84
有权单源最短路
无权单源最短路
带权全源最短路
如果是无权单源,可以BFS+队列,有权就不行了
带权单源最短路
Dijkstra
视频演示:
【算法】最短路径查找—Dijkstra算法_哔哩哔哩_bilibili
各个算法用途比较
AcWing 849. Dijkstra求最短路 I - AcWing 代码详解
朴素版
C++
#include<iostream> #include<algorithm> #include<cstring> using namespace std; const int N=510; int g[N][N]; //为稠密阵所以用邻接矩阵存储 int dist[N]; //用于记录每一个点距离第一个点的距离 bool st[N]; //用于记录该点的最短距离是否已经确定 int n,m; int Dijkstra() { memset(dist, 0x3f,sizeof dist); //初始化距离 0x3f代表无限大 dist[1]=0; //第一个点到自身的距离为0 for(int i=0;i<n;i++) //有n个点所以要进行n次 迭代 { int t=-1; //t存储当前访问的点 for(int j=1;j<=n;j++) //这里的j代表的是从1号点开始 if(!st[j]&&(t==-1||dist[j]<dist[t])) t=j; st[t]=true; for(int j=1;j<=n;j++) //依次更新每个点所到相邻的点路径值 dist[j]=min(dist[j],dist[t]+g[t][j]); } if(dist[n]==0x3f3f3f3f) return -1; //如果第n个点路径为无穷大即不存在最低路径 return dist[n]; } int main() { cin>>n>>m; memset(g,0x3f,sizeof g); //初始化图 因为是求最短路径 //所以每个点初始为无限大 while(m--) { int x,y,z; cin>>x>>y>>z; g[x][y]=min(g[x][y],z); //如果发生重边的情况则保留最短的一条边 } cout<<Dijkstra()<<endl; return 0; } |
对于「稠密图」,应当使用「朴素版」,对于「稀疏图」,应当使用「优先队列版」
堆优化版
(vector邻接表,但是大多用的还是数组模拟邻接表)
C++
/** 邻接表存的话不用考虑重边,dij算法会取最小的 **/ #include<bits/stdc++.h> using namespace std; typedef pair<int,int>PII; const int N = 1e5 + 10; // 稀疏图用邻接表来存 vector<vector<PII>>G; int dist[N]; bool st[N]; int n,m; int dijkstra(){ memset(dist,0x3f,sizeof dist); dist[1] = 0; // 这里heap中为什么要存pair呢,首先小根堆是根据距离来排的,所以有一个变量要是距离, // 其次在从堆中拿出来的时候要知道知道这个点是哪个点,不然怎么更新邻接点呢?所以第二个变量要存点。 // 这个顺序不能倒,pair排序时是先根据first,再根据second, // 这里显然要根据距离排序,对pair排序默认先对first,所以first放距离更省事 priority_queue<PII,vector<PII>,greater<PII>>heap; heap.push({0,1}); while(heap.size()){ auto t = heap.top(); heap.pop(); int distance = t.first,node = t.second; if(st[node])continue; st[node] = true; for(int i = 0; i < G[node].size(); i ++){ int newNode = G[node][i].first; int len = G[node][i].second; if(dist[newNode]>dist[node] + len){ dist[newNode] = dist[node] + len; heap.push({dist[newNode],newNode}); } } } if(dist[n] == 0x3f3f3f3f)return -1; else return dist[n]; } int main(){ cin >> n >> m; G.resize(n + 1); for(int i = 0; i < m; i ++){ int x,y,z; cin >> x >> y >> z; G[x].push_back({y,z}); } cout << dijkstra(); return 0; } |
最短路径 和 各种第二标量
例题PTA甲级 紧急事件
C++
#include<bits/stdc++.h> using namespace std; int G[510][510]; int dist[510]; int num[510];//救援队 int n,m,c1,c2; bool vis[510]; int weight[510]; int num2[510];//最短路数量 void dijkstra(){ memset(dist,0x3f,sizeof dist); dist[c1] = 0; num2[c1] = 1; num[c1] = weight[c1]; for(int i = 0; i < n ; i ++){ int t = -1; for(int j = 0; j < n; j ++){ if(vis[j]==false&&(t==-1||dist[t] >dist[j])){ t = j; } } vis[t] = true; for(int j = 0; j < n; j ++){ // dist[j] = min(dist[j],dist[t] + G[t][j]); if(dist[t] + G[t][j] < dist[j]){ dist[j] = dist[t] + G[t][j]; num[j] = num[t] + weight[j]; num2[j] = num2[t]; }else if(dist[t] +G[t][j] == dist[j]){ num2[j] += num2[t]; if(weight[j] + num[t] > num[j]) num[j] = num[t] + weight[j]; } } } cout << num2[c2] <<" "<<num[c2]; } int main(){ cin >>n >> m >>c1>>c2; for(int i = 0; i < n; i ++){ cin >> weight[i]; } memset(G,0x3f,sizeof G); while(m --){ int x,y,z; cin >> x >> y >> z; G[x][y] = min(G[x][y],z); G[y][x] = min(G[y][x],z); // G[x][y] = G[y][x]= z; } dijkstra(); return 0; } |
书P375
打印路径
C++
void DFS(int s,int v){ //s是起点编号,v是当前访问的顶点编号(从终点开始递归) if(v == s){ printf("%d\n",s); return ; } DFS(s,pre[v]); printf("%d\n",v); } |
为什么无法处理负边
https://www.acwing.com/solution/content/6320/详解很好
bellman-ford
与spfa比可能唯一的好处:如果是有边数限制的话,就不能用spfa了
时间复杂度 N*M
如果有负权回路的话,最短路不一定存在了
bellman-ford是可以求出是否有负环的,但平时不用,时间复杂度太高,后面SPFA会用到
鸽巢(抽屉)原理
Spfa
多源汇最短路
Floyd
4.第四章 数学知识(12.10)
进制转换
(要很熟练)
转换成10进制用的秦九韶算法,迭代方式提升效率
位运算
洛谷 P1469 找筷子
CPP
#include<bits/stdc++.h> using namespace std; //交换律 结合律 把偶数的先结合成0,最后只剩落单的和0异或还是它自己 int ans,n,a;//ans是所有数异或之后的结果,也就是题目所求的落单的筷子的数目 int main() { cin>>n; for (int i=1;i<=n;i++) { scanf("%d",&a); ans^=a;//把所有的数都异或起来 } printf("%d",ans); return 0; } |
加法原理与乘法原理
结果可能很大,要对指定数字取余数。不能全部乘完之后再取余数,因为中间结果可能会溢出。可以乘完一次就取一次余数。实际上,加法或者乘法都可以这样做:
(a+b+c)%k = ((a+b)%k + c)%k
(a * b * c) % k = ((ab)%k * c)%k
组合数问题
例题
洛谷 P2822 [NOIP2016 提高组] 组合数问题
| 同余定理,前面是除数,后面是被除数
杨辉三角的性质
C++
#include<bits/stdc++.h> using namespace std; long long c[2010][2010]; int main(){ int t,k,m,n; cin >> t >> k; //先设置一个杨辉三角 for(int i = 0; i <= 2000; i ++){ c[i][0] = c[i][i] = 1; for(int j= 1; j < i ; j ++) //加法计算可以在途中取余 c[i][j] = (c[i-1][j] + c[i-1][j-1])%k; } while(t--){ int ans = 0; cin >> n >> m; for(int i = 0; i <= n; i ++) for(int j = 0; j <= min(i,m); j ++) ans+=c[i][j] == 0; cout << ans << endl; } return 0; } |
约数
例题 P2926 [USACO08DEC] Patting Heads S
未解决
试除法求约数
约数个数和约数之和(两个公式)
约数个数证明:
质数和合数
埃氏筛
C++
void get_primes1(){ for(int i=2;i<=n;i++){ if(!st[i]){ primes[cnt++]=i; for(int j=i;j<=n;j+=i) st[j]=true;//可以用质数就把所有的合数都筛掉; } } } |
线性筛
C++
#include <iostream> #include <algorithm> using namespace std; const int N = 1000010; //primes数组用来存放质数 int primes[N], cnt; //st[i], i为质数则为false否则为true bool st[N]; void get_primes(int n) { for(int i = 2; i <= n; i++) { if(!st[i]) primes[cnt++] = i; //假设primes[0]为n最小的质因子,i为最大的因数, //易知若primes[i]中i>0,则会进入循环后产生多余的标记。 for(int j = 0; primes[j] <= n / i; j ++) { //标记;primes[j]一定是primes[j]*i的最小质因子 st[primes[j]*i] = true; //表明primes[j]一定是i的最小质因子,没有必要再遍历,primes要小于等于i的最小质因子 //这样能保证每个数遍历一遍,而没有重复 if(i % primes[j] == 0) break; } } } int main() { int n; cin >> n; get_primes(n); cout << cnt << endl; return 0; } |
快速幂
这个博客很好
模板
CPP
#include<bits/stdc++.h> using namespace std; int n; long long int qmi(long long int base, long long int power, long long int p) { long long int result = 1; while (power > 0) { if (power & 1) result = result * base % p; //根据公式每个项都取余数后在再做累乘 base = base * base % p ; //根据公式每个项都取余数后在再做平方操作 power >>= 1; } //根据公式在最后的的结果上再来一次取余数 return result % p; } int main(){ cin >> n; while(n --){ int base,power,p; cin >> base >> power >> p; cout << qmi(base,power,p)<<endl; } return 0; } |
n mod 2可以写成n & 1, n/2 可以写成 n >> 1
应用
5.第五章 动态规划(待更)
重复调用的问题适合改动态规划
斐波那契数列优化
1.递归
2.记忆化
3.dp
4.使用两个变量
动态规划就是递推的子集
背包问题
01背包问题
一维 01 逆序,完全 正序
记化化搜索
二维DP
C++
#include<bits/stdc++.h> using namespace std; const int MAXN = 1005; int v[MAXN]; // 体积 int w[MAXN]; // 价值 int f[MAXN][MAXN]; // f[i][j], j体积下前i个物品的最大价值 int main() { int n, m; cin >> n >> m; for(int i = 1; i <= n; i++) cin >> v[i] >> w[i]; for(int i = 1; i <= n; i++) for(int j = 1; j <= m; j++) { // 当前背包容量装不进第i个物品,则价值等于前i-1个物品 if(j < v[i]) f[i][j] = f[i - 1][j]; // 能装,需进行决策是否选择第i个物品 else f[i][j] = max(f[i - 1][j], f[i - 1][j - v[i]] + w[i]); } cout << f[n][m] << endl; return 0; } |
滚动数组(优化成一维DP)版本
CPP
for(int i = 1; i <= n; i++) //这里如果从0~m的话,可能会用到上层被这层覆盖的值 //可以自己画表 for(int j = m; j >= 0; j--) { if(j < v[i]) f[i][j] = f[i - 1][j]; // 优化前 f[j] = f[j]; // 优化后,该行自动成立,可省略。 else f[i][j] = max(f[i - 1][j], f[i - 1][j - v[i]] + w[i]); // 优化前 f[j] = max(f[j], f[j - v[i]] + w[i]); // 优化后 |
完全背包
二维
CPP
#include<bits/stdc++.h> using namespace std; const int P = 1e3 + 10; int N,V; int v[P],w[P]; int mm[P][P]; // int dfs(int x,int spV){ // if(x > N || spV < 0)mm[x][spV] = 0; // else if(spV < v[x]) mm[x][spV]=dfs(x+1,spV); // return mm[x][spV]; // else mm[x][spV] = max(dfs(x + 1,spV),dfs(x,spV-v[x])+w[x]); // } int main(){ cin >> N >> V; for(int i = 1; i <= N; i ++){ cin >> v[i] >> w[i]; } // int res = dfs(1,V); for(int i = 1; i <= N; i ++){ for(int j = 1; j <= V; j ++){ if(j < v[i])mm[i][j] = mm[i - 1][j]; else mm[i][j] = max(mm[i - 1][j],mm[i][j - v[i]]+ w[i]); } } cout << mm[N][V]; return 0; } //二维正序枚举,下面的写法不舒服,可以把上面的改成正序的 #include<iostream> using namespace std; const int N = 1010; int f[N][N]; int v[N],w[N]; int main() { int n,m; cin>>n>>m; for(int i = 1 ; i <= n ;i ++) { cin>>v[i]>>w[i]; } for(int i = 1 ; i<=n ;i++) for(int j = 0 ; j<=m ;j++) { for(int k = 0 ; k*v[i]<=j ; k++) f[i][j] = max(f[i][j],f[i-1][j-k*v[i]]+k*w[i]); } cout<<f[n][m]<<endl; } |
一维
可以画图,体积从正序枚举是对的,每个物品可以无限取,从倒序枚举是错的,不然和01(只能取一次)一样了,画个图就ok
CPP
#include<iostream> using namespace std; const int N = 1010; int f[N]; int v[N],w[N]; int main() { int n,m; cin>>n>>m; for(int i = 1 ; i <= n ;i ++) { cin>>v[i]>>w[i]; } for(int i = 1 ; i<=n ;i++) //j小于v[i]的不改变 for(int j = v[i] ; j<=m ;j++) { f[j] = max(f[j],f[j-v[i]]+w[i]); } cout<<f[m]<<endl; } |
多重背包
多重背包问题 I (数据范围100,n^3没超1e7-1e8)
C++
#include <iostream> #include <algorithm> using namespace std; const int N = 110; int v[N], w[N], s[N]; int f[N][N]; int n, m; int main(){ cin >> n >> m; for(int i = 1; i <= n; i ++) cin >> v[i] >> w[i] >> s[i]; for(int i = 1; i <= n; i ++){//枚举背包 for(int j = 1; j <= m; j ++){//枚举体积 //k从0开始,可能一个都不选 for(int k = 0; k <= s[i]; k ++){ if(j >= k * v[i]){ f[i][j] = max(f[i][j], f[i - 1][j - k * v[i]] + k * w[i]); } } } } cout << f[n][m] << endl; return 0; } |
多重背包问题 II (正常做会超)
分组背包
二维
C++
#include<bits/stdc++.h> using namespace std; const int N=110; int f[N][N]; //只从前i组物品中选,当前体积小于等于j的最大值 int v[N][N],w[N][N],s[N]; //v为体积,w为价值,s代表第i组物品的个数 int n,m,k; int main(){ cin>>n>>m; for(int i=1;i<=n;i++){ cin>>s[i]; for(int j=0;j<s[i];j++){ cin>>v[i][j]>>w[i][j]; //读入 } } for(int i=1;i<=n;i++){ for(int j=0;j<=m;j++){ f[i][j]=f[i-1][j]; //不选 for(int k=0;k<s[i];k++){ if(j>=v[i][k]) f[i][j]=max(f[i][j],f[i-1][j-v[i][k]]+w[i][k]); } } } cout<<f[n][m]<<endl; } |
一维
C++
因为只用到了第i-1列,所以可以仿照01背包的套路逆向枚举体积 #include<bits/stdc++.h> using namespace std; const int N=110; int f[N]; int v[N][N],w[N][N],s[N]; int n,m,k; int main(){ cin>>n>>m; for(int i=0;i<n;i++){ cin>>s[i]; for(int j=0;j<s[i];j++){ cin>>v[i][j]>>w[i][j]; } } for(int i=0;i<n;i++){ for(int j=m;j>=0;j--){ for(int k=0;k<s[i];k++){ //for(int k=s[i];k>=1;k--)也可以 if(j>=v[i][k]) f[j]=max(f[j],f[j-v[i][k]]+w[i][k]); } } } cout<<f[m]<<endl; } |
二维费用背包
例题 洛谷 1130红牌
背包问题的变种
01背包求方案数
例题 acwing278. 数字组合
CPP
#include<bits/stdc++.h> using namespace std; const int N = 1e4 + 10; int n,m; int a[N]; int f[N][N]; /*int dfs(int u,int spSum){ if(f[u][spSum]!=0)return f[u][spSum]; else if(spSum != 0 && u > n)f[u][spSum] = 0; else if(spSum < 0)f[u][spSum] = 0; else if(spSum == 0)f[u][spSum] = 1; else f[u][spSum] = dfs(u +1,spSum) + dfs(u + 1,spSum - a[u]); return f[u][spSum]; }*/ int main(){ cin >> n >>m; for(int i = 1; i <= n; i ++){ cin >> a[i]; } for (int i = 0; i <= n+1; i++) { f[i][0] = 1; // 当和为0时,方案数为1 } for (int i = n; i >= 1; i--) { for (int j = 1; j <= m; j++) { f[i][j] = f[i + 1][j]; // 不选择第i个数 if (j >= a[i]) { f[i][j] += f[i + 1][j - a[i]]; // 选择第i个数 } } } cout << f[1][m]; return 0; } |
01背包求具体方案
为什么逆序枚举?
https://blog.csdn.net/yl_puyu/article/details/109960323解释很好
CPP
#include <iostream> #include <algorithm> using namespace std; const int N = 1005; int n, m; int v[N], w[N]; int f[N][N]; int main() { cin >> n >> m; for (int i = 1; i <= n; ++i) cin >> v[i] >> w[i]; for (int i = n; i >= 1; --i) for (int j = 0; j <= m; ++j) { f[i][j] = f[i + 1][j]; if (j >= v[i]) f[i][j] = max(f[i][j], f[i + 1][j - v[i]] + w[i]); } // 在此,f[1][m]就是最大数量 int j = m; for (int i = 1; i <= n; ++i) if (j >= v[i] && f[i][j] == f[i + 1][j - v[i]] + w[i]) { cout << i << ' '; j -= v[i]; } return 0; } |
01背包求最优方案数
背包例题
洛谷P1802 5倍经验日(01背包变形问题)
C++
这是一道变了形的01背包 首先我们因为和每个人打都一定有经验所以一定都要打一遍。 所以不难想到max=lose[1]+lose[2]......+lose[n]+某些磕了药打赢的多出的经验值 因此我们可以进行一个转换,把价值记为win[i]-lose[i],溶剂就是要打赢磕的药,然后要使价值总和最大,然后就变成了基础的零一背包了。。。 #include<iostream> #include<cstring> #include<string> #include<cmath> #include<algorithm> #include<cstdio> using namespace std; int a[100005]; long long f[1000005]; int win[100005]; int v[100005]; int lose[100005]; int main() { int n,m; int sum=0; scanf("%d%d",&n,&m); for (int i=1;i<=n;i++){ scanf("%d%d%d",&lose[i],&win[i],&v[i]); a[i]=win[i]-lose[i]; sum=sum+lose[i]; } for (int i=1;i<=n;i++){ for (int j=m;j>=v[i];j--){ f[j]=max(f[j],f[j-v[i]]+a[i]); } } printf("%lld",5*(f[m]+sum)); return 0; } |
卡特兰数
https://leetcode.cn/circle/discuss/lWYCzv/
例题1 进出栈序列