写在前面(会不定时更新)
-
这份资料大约40%的内容来自平时手记,剩下的内容来自网络(主要是CSDN)。在对应部分给出了内容的来源或者我觉得写的不错的博客的网址,感谢这些博主的博客,如果没有标注完全,敬请原谅,希望原作者看到不要举报我哈(orz)。
-
由于我不是本专业的学生,对于其中不少的内容和定理的推导(数论博弈论等)并不怎么掌握,内容很可能有错。这份资料产生的初衷也只是我想把平时遇到的知识点记录方便复习和查找;备考3个月不到,这份资料的规模也比较大了,加上网上关于蓝桥杯考点的资料很少,因此决定把这份资料公开,供后来的同学们参考和使用。
-
本份材料主要针对蓝桥杯,内容上会略微超出蓝桥杯历年考试的知识点范围,但是实际上这几年蓝桥杯的难度一直在不断上升,关于超纲不超纲的,已经很难界定了;
-
之后可能不会有对这份资料更多的修改了,如果有同学对这份资料的内容感兴趣并且打算进一步完善的同学可以联系我QQ2993349277
-
关于蓝桥杯
- 这几年的蓝桥杯不再是以往的暴力杯,如果有同学在考虑要不要参加,可以尝试21年C/C++A组的大题(DP、博弈论)
- 对于自己的编程的逻辑性和思维的活跃程度,可以尝试一下往年蓝桥省赛试题**外卖优先级和回文日期**这两道题属于模拟类型的题目,但是对于新手来说是有一定的思考难度的,可以自测一下;
- 蓝桥杯官网上有练习系统,可以在上面进行练习,但是官网不提供题解,这里再推荐几个OJ网站;
- 书籍:推荐使用《深入浅出程序设计竞赛 - 基础篇》和刘汝佳的**《算法竞赛入门经典》**,当然对于像我这样时间比较吃紧的同学看书还是比较慢的,并且没有一定的基础阅读也是相当困难。
- 课程:
- 我们学校是不组织蓝桥杯比赛的,如果有想要参加的话很有可能需要自费参加;请有想法的同学权衡好时间,费用和比赛作用。
-
关于资料
- 这份资料上的模板可以随意使用,请勿作任何商用;
- 资料上不少地方没有严格跑过,很可能有错误,但是思想基本无误,请大家原谅;
- 整份资料基于C/C++编写,其他语言也适用,以思想为主;
- 不错的知识点网站(已经不止是不错了):
- 这份资料上的大部分代码都采用了适合初学者的一些数据结构以及实现:
- 例如邻接表采用vector数组存放而没有采用链式前向行算法(为什么不问问神奇的STL呢?);
- 最短路问题中的单源最短路径dijsktra直接采用了priority_queue进行堆优化,和课本中略不同;
- 最短路问题中处理负边权的Bellman-ford算法采用了SPFA算法,但是SPFA只能在负边权使用,否则卡死;
- 关于滚动数组优化动态规划并不推荐,在理解上没有多维形象;
- 关于线段树部分整理比较乱,对于比赛更推荐树状数组(代码短),对于特定问题树状数组不容易出错且工程小;
- 关于离散化的部分在本资料中少有提及,主要在树状数组的逆序对与K小/大值问题中有应用。
-
关于学习
- 对于基础比赛和基础算法题,都是一些熟练活,常用的数据结构多写;
- 可以多刷一些洛谷上提高及以下难度的题目,对于具有省选难度的题目可适当选择;
- 刷题不要随便刷,最好能够结合固定知识点和题单,向薄弱知识点猛攻;
- 算法的学习是很困难的,有效地学习,有效地刷题很关键,最好能有一对一的辅导,这当然很困难;但是可以尝试找个一起的小伙伴,甚至可以进一步去尝试ACM等更高的比赛【当然这已经是远超蓝桥杯水平的程度了】。
- 网上很多同学贡献了非常多的代码模板和代码解析,在遇到的过程中注意收集和总结。
----------------------------------------------------------------------------------------------------------------------------------------------------------2022年4月4日
赛后
??????
今年因为线上比赛,赛方为了防止作弊,题型改为了2填空+8大题(分值10+140)。坏了成OI杯了。
今年的题目感觉数据结构与算法这次考察的不多,博弈、DP、二分答案最后一题应该是树状数线段树之类的,但是我没有做…………
现在突然感觉这份资料有点鸡肋啊…………
总体做下来感觉不是很好,感觉大题会爆零,不过现在再怎么怎么想也没有用了…………
一切都是为了上研,有没有的都要继续我的考研生活了…………
未来要参加的同学,如果看到这篇文章,那么我觉得可能前面和后面的内容可能都有点无关紧要了…………
就再给大家一些建议吧:
- 动态规划想学好,多做题,多套闫氏DP分析法进行分析;
- 不要小看逻辑简单(未必简单)只是规模较大的题目,这些题目可以很好地锻炼逻辑分析能力和抽象能力;
- 学会暴力,暴力是一种手段更是一门学问,这几年暴力可能打不通,但是对于这种比赛,暴力往往是拿分的有效手段;
- 多刷题,刷模拟类的题目;多刷题,锻炼手速和代码能力;多刷题,锻炼思维能力…………;
- 真的要多刷题,如果可以的话可以做做acwing周赛;这些大小比赛不仅需要能力,更需要策略,题目的难度未必是递增的,你要如何分配你有限的时间,也只能在做题中感受出来。
现在只能是祈祷能混个奖了。
-----------------------------------------------------------------------------------------------------------------------------------------------------2022年4月9日
1. 排序
比较重要的有快排(可sort实现)、归并排序(求解逆序对,可采用树状数组)、拓扑排序(图论问题求解);
数据相关基本上直接调用sort就可以了,sort底层是用快排实现的,速度还是比较快的;还有prior_queue优先队列
sort(vector.begin(), vector.end());
sort(v,v+n,cmp);
sort(a,a+n);
bool cmp(const type& A, const type& B){
return A > B;
}
拓扑排序
拓扑排序思路:
- 开一个数组存放所有顶点的值
- 邻接表存储边和点;根据邻接表开数组存储每个节点的入度
- 通过队列,类似BFS过程,把所有入度为0的点加入队列
- 处理弹出的点的连接的边,过程中入度为0的点进入队列
- 拓扑排序也可以反向建立图
//杂务
//拓扑排序+DP
#include <iostream>
#include <ctime>
#include <vector>
#include <cstdlib>
#include <sstream>
#include <algorithm>
#include <cstring>
#include <queue>
using namespace std;
int n, x;
vector<int> v[10001];//v[i]:其中的元素在等待i号任务完成
int tim[10001];
int rudu[10001];
int dp[10001];
inline int max(int a, int b){
return a>b?a:b;
}
void test01()
{
//初始化
cin >> n;
memset(rudu, 0, sizeof(rudu));
int index = 0;
for(int i = 1; i <= n; ++i){
cin >> index;
cin >> tim[index];
//创建拓扑序列
while(cin >> x){
if(x != 0){
v[x].push_back(index); //反向建立:index等待x完成
rudu[index]++; //index多一个前驱
}else{
break;
}
}
}
//--------------------------------------
//拓扑排序
queue<int> q;
for(int i = 1; i <= n; ++i){
if(!rudu[i]){ //入度为0,说明可以完成
q.push(i);
dp[i] = tim[i]; //动归数组起点
}
}
while(!q.empty()){
int now = q.front();
q.pop();
for(int i = 0; i < v[now].size(); ++i){
int nex = v[now][i]; //nex元素等待now元素完成
dp[nex] = max(dp[nex], dp[now]+tim[nex]); //更新最长时间
rudu[nex]--;
if(!rudu[nex]){
q.push(nex);
}
}
}
int ans = 0;
for(int i = 1; i <= n; ++i){
ans = max(ans, dp[i]);
}
cout << ans << endl;
}
int main()
{
test01();
return 0;
}
https://www.luogu.com.cn/problem/solution/P1113
上面是一篇非常好的题解和例题
https://xulidong.blog.csdn.net/article/details/41809119?spm=1001.2101.3001.6650.1&utm_medium=distribute.pc_relevant.none-task-blog-2%7Edefault%7ECTRLIST%7ERate-1.pc_relevant_antiscanv2&depth_1-utm_source=distribute.pc_relevant.none-task-blog-2%7Edefault%7ECTRLIST%7ERate-1.pc_relevant_antiscanv2&utm_relevant_index=2
归并排序(求逆序对)
归并排序原理就是:对原来已经有序的两个数组进行合并操作 (取两个头部比较) 后还是有序的.
主要用来做排序和求逆序对,但是逆序对使用树状数组更好
void merge_sort(int s,int t){
//s =start t=T
int mid,i,j,k;
if(s==t) return ; //如果区间只有一个数,就返回
mid = (s+t)>>1; //取中间的点
merge_sort(s,mid);
merge_sort(mid+1,t);
i=s,k=s,j=mid+1;
while(i<=mid && j<=t){
if( a[i] <=a[j]){
tmp[k]=a[i];k++;i++;
} else {
tmp[k]=a[j];j++;k++;
}
}
while(i<=mid) { tmp[k]=a[i];k++;i++;}
while(j<=t) { tmp[k]=a[j];k++;j++;}
for(i=s;i<=t;i++)
a[i]=tmp[i];
}
2. 递归
递归算法
一种直接或者间接调用自身函数或者方法的算法。递归算法的实质是把问题分解成规模缩小的同类问题的子问题,然后递归调用方法来表示问题的解。递归算法对解决一大类问题很有效,它可以使算法简洁和易于理解。
特点
1)递归就是方法里调用自身。
2)在使用递增归策略时,必须有一个明确的递归结束条件,称为递归出口。
3)递归算法解题的运行效率较低。所以一般不提倡用递归算法设计程序。
4)在递归调用的过程当中系统为每一层的返回点、局部量等开辟了栈来存储。递归次数过多容易造成栈溢出等,所以一般不提倡用递归算法设计程序。
数组求和
int a[maxn];
int getsum(int n){
if(n == 1){
return a[1];
}
return getsum(n-1)+a[n];
}
菲波那切数列
int fib(int n){
if(n == 1 || n == 2){
return 1;
}
return fib(n-1)+fib(n-2);
}
求阶乘
int jiecheng(int n){
if(n == 0 || n == 1){
return 1;
}
return jiecheng(n-1)*n;
}
3. 动态规划(重、难点)
解题步骤:
1. 定义子问题
2. 写出子问题的递推关系
3. 确定 DP 数组的计算顺序
4. 空间优化(可选)
动态规划本质是递推,核心是找到状态转移的方式,写出dp方程
此外,打表是学习和解决动态规划问题的一种很好的方法
闫氏DP分析法
暂时没有看,但是据说所有的DP问题都可以用该方法分析
常用的DP数组的开辟方法与思路
//MLGB想不出来就硬套,一个一个试
//1. 线型DP
最长上升子序列:
for (i=2;i<=n;i++){//当前所在的位置
for(j=1;j<i;j++){//询问当前所在位置前面的位置
if( a[j] <= a[i] && f[i] < f[j]+1){
f[i] = f[j]+1;
if(max < f[i])
max = f[i];
}
}
}
最长公共子序列:
for(i=1;i<=l1;i++)
for(j=1;j<=l2;j++){
if( s1[i] == s2[j]){
f[i][j] = f[i-1][j-1]+1;
}
else
f[i][j] = max(f[i][j-1],f[i-1][j]);
}
return f[l1][l2];
//2. 区间DP
//3. 环形DP
//4. 树与图上的DP
//5. 状压DP
//6. 数位DP
具体到洛谷上的题单刷题吧
***概率DP(**比较难,还是别看了,还有其他期望相关的,大概率不考)
- 有n个投资事件和一个成功概率最低接受值rate。
- 每个投资的价值是c[i],成功概率是p[i](浮点数)。
- 在保证成功概率≥rate的情况下,使价值最大化。
- 状态转移方程:dp[j]=max(dp[j],dp[j-c[i]]*p[i]);
const int INF = 0x3f3f3f3f;
const int MAX = 100005;
using namespace std;
double p[MAX],dp[MAX],rate; //
int c[MAX]; //数组
int main()
{
int n,i,j,t,v;
cin >> t;
while(t--)
{
cin >> rate >> n;
//dp[i]表示价值为i时的成功率
memset(dp,0,sizeof(dp));
dp[0]=1.0; //价值为0的成功概率
v=0;//投资的总和
for(i=1;i<=n;i++)
{
scanf("%d%lf",&c[i],&p[i]);
v+=c[i]; //所有投资的总和
}
//反向遍历,这是根据更新顺序决定的
for(i=1;i<=n;i++)
for(j=v;j>=c[i];j--) //每个价值所对应的最大成功概率
dp[j]=max(dp[j],dp[j-c[i]]*p[i]);
for(i=v;i>=0;i--)
if(dp[i]>=rate)//第一个大于rate的值
{
printf("%d\n",i);
break;
}
}
return 0;
}
https://www.cnblogs.com/kuangbin/archive/2012/10/02/2710606.html
线型DP
- LIS最长不下降子序列
找到最长的单调递增的子序列
# 输入样例
6
3 4 1 2 3 6
# 输出样例
4
# 样例解释
(1 2 3 6) 为最长的上升子序列
解题思路
这个问题的是: 求序列的最长序列的长度 如何把这个问题分解成子问题呢?经过分析,发现:
求以 ak(k=1,2,3,…N)为终点的最长上升子序列的长度是个好的子问题.
问题解决建立在局部最优的前提下
前面的i项均已经取得前i项可求得的最长上升子序列,再加入新的点,只需要不断向前询问即可,找找既比前面的人大,又有较大值的进行更新
DP方程:
f[k] = max(f[k] ,f(i)+1) if 1 < i < k && a[i] < a[k]
#include <cstdio>
int a[1000];
int n;
int f[1000];
int main(){
scanf("%d",&n);
int i;
int j;
for (i=1;i<=n;i++){
scanf("%d",&a[i]);
}
//每个点的f值不可能小于1
for(i=1;i<=n;i++) f[i] =1;
int max = 1;//这里是1,想想为什么
for (i=2;i<=n;i++){//当前所在的位置
for(j=1;j<i;j++){//询问当前所在位置前面的位置
if( a[j] <= a[i] && f[i] < f[j]+1){
f[i] = f[j]+1;
if(max < f[i])
max = f[i];
}
}
}
printf("%d",max);
return 0;
}
核心(O(n2))
int max = 1;//这里是1,想想为什么
for (i=2;i<=n;i++){
for(j=1;j<i;j++){
if( a[j] <= a[i] && f[i] < f[j]+1){
f[i] = f[j]+1;
if(max < f[i])
max = f[i];
}
}
}
二分优化O(nlogn)
//二分查找距离最近的偏小值
using namespace std;
typedef long long ll;
const int maxn = 1e6+5,maxe = 1e6+5; //点与边的数量
int n,m;
int a[maxn]; // 原数组
int c[maxn]; //c[i]表示: 所有 lis值 == i 那些元素中值最那个小的那个
int f[maxn]; // f[i] = 第i个元素的lis值
void lis(){
memset(c,0x7f,sizeof(c));
f[1] = 1;
c[1] = a[1];
for(int i=2;i<=n;++i){
f[i] = upper_bound(c+1, c+1+n, a[i]) - c;
c[f[i]] = min(c[f[i]],a[i]);
}
}
int main(){
scanf("%d",&n);
for(int i=1;i<=n;++i)
scanf("%d",&a[i]);
lis();
for(int i=1;i<=n;++i)
printf("%d %d\n",i,f[i]);
return 0;
}
- LCS最长公共子序列
逻辑:依旧是局部最优推导全局最优
不断向两个字符串中加入新的元素
第一行的前i个和第二行的前j个可以得到当前的最长子序列长度
DP方程:
f[i][j] = max(f[i-1][j], f[i][j-1], f[i-1][j-1] if a[i] == b[j]);
#include <cstdio>
#include <cstring>
char s1[5010];
char s2[5010];
int l1,l2;
int f[5010][5010] = {0};
int max(int a,int b){
if( a > b)
return a;
return b;
}
int lcs(){
int i,j;
for(i=1;i<=l1;i++)
for(j=1;j<=l2;j++){
if( s1[i] == s2[j]){
f[i][j] = f[i-1][j-1]+1;
}
else
f[i][j] = max(f[i][j-1],f[i-1][j]);
}
return f[l1][l2];
}
int main(){
scanf("%s",s1+1);
scanf("%s",&s2[1]);
l1 = strlen(s1+1);//求长度
l2 = strlen(s2+1);
int m = lcs();
printf("%d",m);
return 0;
}
- 最大连续子序列和
这个貌似比较复杂,先不写了
区间DP
顾名思义是在区间上 DP,它的主要思想就是先在小区间进行 DP 得到最优解,然后再利用小区间的最优解合并求大区间的最优解。
核心伪代码
//mst(dp,0) 初始化DP数组
for(int i=1;i<=n;i++)
{
dp[i][i]=初始值
}
for(int len=2;len<=n;len++) //区间长度
for(int i=1;i<=n;i++) //枚举起点
{
int j=i+len-1; //区间终点
if(j>n) break; //越界结束
for(int k=i;k<j;k++) //枚举分割点,构造状态转移方程
{
dp[i][j]=max(dp[i][j],dp[i][k]+dp[k+1][j]+w[i][j]);
}
}
平行四边形优化
可以不看
https://blog.csdn.net/my_sunshine26/article/details/77141398
以合并石子为例
dp[i][j]来表示合并第i堆到第j堆石子的最小代价
DP:dp[i][j] = min(dp[i][j],dp[i][k]+dp[k+1][j]+w[i][j])
括号匹配
用dp[i][j]表示区间[i,j]里最大完全匹配数。
只要得到了dp[i][j],那么就可以得到dp[i-1][j+1]
dp[i-1][j+1]=dp[i][j]+(s[i-1]与[j+1]匹配 ? 2 : 0)
然后利用状态转移方程更新一下区间最优解即可。
dp[i][j]=max(dp[i][j],dp[i][k]+dp[k+1][j])
整数划分问题
状态转移方程为
dp[i][j]=max(dp[i][j],dp[k][j-1]*num[k+1][i])
背包DP
- 01背包
每种物品只有一个,放或不放
例:蒜头君的购物袋+0-1背包
dp[i][j]:前i个物品,总容量不超过j时的最大价值
dp[i][j] = max(dp[i-1][j], dp[i-1][j-c[i]]+w[i]);
void test01()
{
cin >> N >> V;
for(int i = 1; i <= N; ++i){
cin >> w[i] >> c[i];
}
//核心
//横向为总容量,纵向为物品
//dp[i][j]:前i个物品,在容量为j的条件下,最大价值
for(int i = 1; i <= N; ++i){//数量循环,dp[i]
for(int j = 0; j <= V; ++j){//容量循环,dp[i][j]
if(j >= c[i]){ //剩余空间可以存放并存放
//存放物品以及不存放物品时上一个状态的最大值
dp[i][j] = max(dp[i-1][j-c[i]]+w[i], dp[i-1][j]);
}else{ //不能存放
dp[i][j] = dp[i-1][j];
}
}
}
cout << dp[N][V] << endl;
}
空间复杂度优化【一维优化】,一般可以不记这个
看完上面的代码,仔细考虑过代码和状态转移方程后,你会发现一些重要的规律:
- 每一行的状态都需要上一行 (前一层) 的状态推导出来
- 第 i 行的第 j 个状态 f [i][j] 一定是由 f [i-1][j] 和 f [i-1][k] 得到的,且 k 一定小于 j
根据上面的规律,我们可以这样做:
定义一个一维数组 f [j] 表示:f [j] 表示前 i 个物品在容量为 j 的条件下的最大价值
我们可以很容易的得到第一个物品的所有的状态
在处理前 2 个物品的状态的时候从 f [10] 到 f [0] 倒过来处理
上面的操作可以把 01 背包的二维状态压缩到 1 维,节省了空间和代码复杂度.
dp[j] = max(dp[j-c[i]]+w[i], dp[j]);
void test01()
{
cin >> N >> V;
for(int i = 1; i <= N; ++i){
cin >> w[i] >> c[i];
}
for(int i = 1; i <= N; ++i){
for(int j = V; j >= c[i]; j--){
dp[j] = max(dp[j-c[i]]+w[i], dp[j]);
}
}
cout << dp[V] <<endl;
}
多重背包
N种物品,体积为ci,每种物品数量有限为ni,容积为V,价值最大
朴素算法:全部看成不一样的物品,按成01背包
dp[i][v] = max(dp[i][v], dp[i-1][v-kc[i]]+kw [i]);
void test01()
{
cin >> N >> V;
for(int i = 1; i <= N; ++i){
cin >> w[i] >> c[i] >> n[i];
}
for(int i = 1; i <= N; ++i){//种类数量循环
for(int j = 0; j <= V; ++j){//容量循环
for(int k = 0; k <= n[i]; ++k){//单体数量循环
//此处不需要else,k=0时相当于else
if(j >= c[i]*k){ //可以放时
dp[i][j] = max(dp[i-1][j-c[i]*k]+w[i]*k, dp[i-1][j]);
}
}
}
}
cout << dp[N][V] << endl;
}
空间优化【一维空间优化】
void test01()
{
cin >> N >> V;
for(int i = 1; i <= N; ++i){
cin >> w[i] >> c[i] >> n[i];
}
for(int i = 1; i <= N; ++i){
for(int j = V; j >= 0; --j){
for(int k = 0; k <= n[i]; ++k){
if(j >= k*c[i]){
dp[j] = max(dp[j], dp[j-k*c[i]]+k*w[i]);
}
}
}
}
cout << dp[V] << endl;
}
多重背包二进制优化
//多重背包二进制优化
using namespace std;
//1.找到最小k的值
//2.
int N, V;
int n[1001], c[1001], w[1001];
int nc[1001], nw[1001]; //新拆分的组的体积和价值
int dp[1001];
//5 10
//2 1 2
//3 5 3
//2 5 1
//3 4 2
//4 3 8
void test01()
{
cin >> N >> V;
for(int i = 1; i <= N; ++i){
cin >> w[i] >> c[i] >> n[i];
}
int ncnt = 0; //新的物品的个数
//找到最小的k
//所有二进制进制数的权重
for(int i = 1; i <= N; ++i){
int k;
for(k = 1; n[i] - (1 << k) + 1 > 0; ++k){
nc[ncnt] = (1 << (k-1))*c[i]; //1,2,4,8,16,32个i物品的种类和,可以表示出63个物品种类的和
nw[ncnt] = (1 << (k-1))*w[i];
++ncnt;
}
--k;
nc[ncnt] = (n[i]-(1 << k)+1)*c[i]; //最后一个,即x-2^k个
nw[ncnt] = (n[i]-(1 << k)+1)*w[i];
++ncnt;
}
//进行01背包
for(int i = 0; i < ncnt; ++i){
for(int j = V; j >= nc[i]; --j){
if(j >= nc[i]){
dp[j] = max(dp[j], dp[j-nc[i]]+nw[i]);
}
}
}
cout << dp[V] << endl;
}
完全背包
每种物品都有无限个
和多重背包唯一的区别就是max(dp[i][j]),考虑的是前一个放入i后再放入i的情况
时间优化不需要从后向前,直接顺序遍历
而且不需要多此一举使用k,直接dp[i][j] = max(dp[i-1][j], dp[i][j-c[i]]+w[i]),注意此处是dp[i][j-c[i]]
void test01()
{
cin >> N >> V;
for(int i = 1; i <= N; ++i){
cin >> w[i] >> c[i];
}
for(int i = 1; i <= N; ++i){
for(int j = 0; j <= V; ++j){
if(j >= c[i]){
dp[i][j] = max(dp[i-1][j], dp[i][j-c[i]]+w[i]);
}else{
dp[i][j] = dp[i-1][j];
}
}
}
cout << dp[N][V] << endl;
}
空间优化【一维】
void test01()
{
cin >> N >> V;
for(int i = 1; i <= N; ++i){
cin >> w[i] >> c[i];
}
for(int i = 1; i <= N; ++i){
for(int j = 0; j <= V; ++j){
//必须正序,只有正序,dp[i][j-c[i]]才是被更新过的
if(j >= c[i]){
dp[j] = max(dp[j-c[i]]+w[i], dp[j]);
}
}
}
cout << dp[V] << endl;
}
- 多重背包的二进制优化
证明1,2,4,…,2的k-1次方可得到2的k次-1以内的所有数
多重背包问题通常可转化成01背包问题求解。但若将每种物品的数量拆分成多个1的话,时间复杂度会很高,从而导致TLE。所以,需要利用二进制优化思想。即:
一个正整数n,可以被分解成1,2,4,…,2(k-1),n-2k+1的形式。其中,k是满足n-2^k+1>0的最大整数。
例如,假设给定价值为2,数量为10的物品,依据二进制优化思想可将10分解为1+2+4+3,则原来价值为2,数量为10的物品可等效转化为价值分别为12,22,42,32,即价值分别为2,4,8,6,数量均为1的物品。
void test01()
{
cin >> N >> V;
for(int i = 1; i <= N; ++i){
cin >> w[i] >> c[i] >> n[i];
}
int ncnt = 0; //新的物品的个数
//找到最小的k
//所有二进制进制数的权重
for(int i = 1; i <= N; ++i){//物品种类循环
int k;
//相当于创造新的物品
for(k = 1; n[i] - (1 << k) + 1 > 0; ++k){
nc[ncnt] = (1 << (k-1))*c[i]; //1,2,4,8,16,32个i物品的种类和,可以表示出63个物品种类的和
nw[ncnt] = (1 << (k-1))*w[i];
++ncnt;
}
--k;
//二进制整数之外没能表示的部分:1 2 4 8 16 3(这里三无法用二进制整数表示时)
nc[ncnt] = (n[i]-(1 << k)+1)*c[i]; //最后一个,即x-2^k个
nw[ncnt] = (n[i]-(1 << k)+1)*w[i];
++ncnt;
}
//进行01背包
for(int i = 0; i < ncnt; ++i){
for(int j = V; j >= nc[i]; --j){
if(j >= nc[i]){
dp[j] = max(dp[j], dp[j-nc[i]]+nw[i]);
}
}
}
cout << dp[V] << endl;
}
数位 DP 就是说我们 DP 转移的过程和数的位置有关
数型 dp 本质上是在树上行走的过程
树形DP一般很难用线性方式进行搜索,需要使用DFS进行递归;因此更接近于记忆化搜索
记忆化搜索
小总结:
- DP是所有算法中最频繁考察也最麻烦的算法
- DP的关键在于DP方程的寻找 如何进行子问题的划分
- 如何扩展子问题到大问题
- 此外,子问题也是由更小的子问题得来的
- DP方程的一般形式就是
dp[i][j] = max(dp[i][j], 更新方程)
- 最优子结构:
国王相信,只要他的两个大臣能够回答出正确的答案(对于考虑能够开采出的金子数,最多的也就是最优的同时也就是正确的),再加上他的聪明的判断就一定能得到最终的正确答案。我们把这种子问题最优时母问题通过优化选择后一定最优的情况叫做 “最优子结构”。
- 子问题重叠 / 相似子问题:
实际上国王也好,大臣也好,所有人面对的都是同样的问题,即给你一定数量的人,给你一定数量的金矿,让你求出能够开采出来的最多金子数。我们把这种母问题与子问题本质上是同一个问题的情况称为 “子问题重叠”。然而问题中出现的不同点往往就是被子问题之间传递的参数,比如这里的人数和金矿数。
- 边界:
想想如果不存在前面我们提到的那些底层劳动者的话这个问题能解决吗?永远都不可能!我们把这种子问题在一定时候就不再需要提出子子问题的情况叫做边界,没有边界就会出现死循环。
- 子问题独立:
要知道,当国王的两个大臣在思考他们自己的问题时他们是不会关心对方是如何计算怎样开采金矿的,因为他们知道,国王只会选择两个人中的一个作为最后方案,另一个人的方案并不会得到实施,因此一个人的决定对另一个人的决定是没有影响的。我们把这种一个母问题在对子问题选择时,当前被选择的子问题两两互不影响的情况叫做 “子问题独立”。
- 无后效性:
一个某个人解决一个问题,那会这个问题只会影响包含这个问题的更大子问题,但是这个问题后面的问题 (后) 就不会影响这个问题了。即子问题的状态一但确定,就不会再改变.
这就是动态规划,具有最优子结构、子问题重叠、边界和子问题独立 , 无后效性,当你发现你正在思考的问题具备这四个性质的话,那么恭喜你,你基本上已经找到了动态规划的方法。
技巧
很多时候动态规划问题可以用记忆化搜索的方式实现,如果没有思路,那么可以选择搜索+dp的方式,时间复杂度基本一致
当时记忆化搜索并不能解决所以的动态规划问题。
常用的动态规划转移方程:
写出所有的dp[i][j]、dp[i-1][j]、dp[i+1][j]、dp[i][j+1]
等等的含义,想想如何进行转移
1. 线性DP
最长上升、下降子序列:
含义:位置为i时候的最长上升、下降序列长度
for(i = 2; i <= n; ++i){
for(int j = 1; j < i; ++j){
if(a[j] <= a[i] && dp[i] < dp[j]+1){
dp[i] = dp[j] + 1;
}
}
}
最长公共子序列:
含义:sa字符串和sb字符串双指针位置处公共子序列最大长度
线性任务:
含义:从后向前进行遍历,从第i时间段开始的最大休息时间
对于一个任务来说,我可以选择做和不做
如果当前无任务:dp[i] = dp[i+1] + 1;
如果有任务:dp[i] = max(dp[i], dp[i+a[j]]);
编辑距离:
含义:和最长公共子序列相同
for(int i=1;i<=lena;i++)
for(int j=1;j<=lenb;j++) {
if(a[i-1]==b[j-1]){
f[i][j]=f[i-1][j-1];
continue;
}
f[i][j]=min(min(f[i-1][j],f[i][j-1]),f[i-1][j-1])+1;
}
}
大师:
含义:dp[i][j]
表示以i结尾公差为j的等差数列个数
int p=20000;
for(int i=1;i<=n;i++){ //整体遍历
ans++;
for(int j=i-1;j;j--){ //反向遍历,双指针
f[i][a[i]-a[j]+p] += f[j][a[i]-a[j]+p]+1;
f[i][a[i]-a[j]+p] %= mod;
ans += f[j][a[i]-a[j]+p] + 1;
ans%=mod;
}
}
摆花:
含义:dp[i][j]
表示前i种花总数为j的方案数
f[0][0] = 1;
for(int i = 1; i <= n; i++)//种类循环
for(int j = 0; j <= m; j++)//数量循环
for(int k=0; k <= min(j, a[i]); k++) //可选个数
f[i][j] = (f[i][j] + f[i-1][j-k])%mod;
木棍加工:
含义:
sort(m, m + n);
for(int i = 0; i < n; ++i){ //数量循环
for (int j = i - 1; j >= 0; j--){ //反向循环双指针
if (m[i].w > m[j].w) f[i] = max(f[i], f[j] + 1);
}
ans = max(ans, f[i]); //更新ans的值
}
合唱队形:
含义:双向最长上升
//l
for(int i = 1; i <= n; ++i){
for(int j = 1; j <= i; ++j){
if(a[j] < a[i]){
l[i] = max(l[i], l[j]+1);
}
}
}
//r
for(int i = n; i >= 1; --i){
for(int j = n; j >= i; --j){
if(a[j] < a[i]){
r[i] = max(r[i], r[j]+1);
}
}
}
int ans = 0;
for(int i = 1; i <= n; ++i){
ans = max(ans, r[i]+l[i]);
}
金宝剑:
含义:
dp[0][0] = 0;
for(i = 1;i <= n;++i){
for(j = 1;j <= w;++j){
for(k = j - 1;k <= min(j + s - 1,w);++k){
dp[i][j] = max(dp[i][j],dp[i - 1][k] + a[i] * j);
}
}
}
- 区间DP
关路灯:
含义:
f[i][j][0]表示关掉i到j的灯后,老张站在i端点,
f[i][j][1]表示关掉[i][j]的灯后老张站在右端点(i为左端,j为右端)
f[c][c][0]=f[c][c][1]=0;//瞬间被关(初始化)
for(int l=2;l<=n;l++)
for(int i=1;i+l-1<=n;i++)
{
int j=i+l-1;
f[i][j][0]=min(f[i+1][j][0]+(a[i+1]-a[i])*(sum[i]+sum[n]-sum[j]),//继续走下去会更快吗?
f[i+1][j][1]+(a[j]-a[i])*(sum[i]+sum[n]-sum[j]))
//还是从j点折返回来会更快?(此时假设[i+1][j]被关,i亮,从j端点往回赶去关i)
//要注意的一点是sum[n]-(sum[j]-sum[i])是包括了i这一点的电能的,因为走过来的过程中灯i也会耗电
f[i][j][1]=min(f[i][j-1][0]+(a[j]-a[i])*(sum[i-1]+sum[n]-sum[j-1]),//同上
f[i][j-1][1]+(a[j]-a[j-1])*(sum[i-1]+sum[n]-sum[j-1]));
}
合唱队形:
含义:
f[i][j][0]表示的是第i人从左边进来的方案数
f[i][j][1]表示的是第j人从右边进来的方案数
for(int i=1;i<=n;i++)f[i][i][0]=1;
for(int len=1;len<=n;len++)
for(int i=1,j=i+len;j<=n;i++,j++){
if(a[i]<a[i+1])f[i][j][0]+=f[i+1][j][0];
if(a[i]<a[j])f[i][j][0]+=f[i+1][j][1];
if(a[j]>a[i])f[i][j][1]+=f[i][j-1][0];
if(a[j]>a[j-1])f[i][j][1]+=f[i][j-1][1];
f[i][j][0]%=19650827;
f[i][j][1]%=19650827;
}
}
合并石子:
含义:区间合并
for(int p=1;p<n;p++)
{
for(int i=1,j=i+p;(j<n+n) && (i<n+n);i++,j=i+p)
{
f2[i][j]=999999999;
for(int k=i;k<j;k++)
{
f1[i][j] = max(f1[i][j], f1[i][k]+f1[k+1][j]+d(i,j));
f2[i][j] = min(f2[i][j], f2[i][k]+f2[k+1][j]+d(i,j));
}
}
}
minl=999999999;
for(int i=1;i<=n;i++)
{
maxl=max(maxl,f1[i][i+n-1]);
minl=min(minl,f2[i][i+n-1]);
}
能量项链:
含义:区间合并
for(int i=2;i<2*n;i++){
for(int j=i-1;i-j<n&&j>=1;j--){//从i开始向前推
for(int k=j;k<i;k++)//k是项链的左右区间的划分点
s[j][i]=max(s[j][i],s[j][k]+s[k+1][i]+e[j]*e[k+1]*e[i+1]);
//状态转移方程:max(原来能量,左区间能量+右区间能量+合并后生成能量)
if(s[j][i]>maxn)maxn=s[j][i];//求最大值
}
}
矩阵取数游戏:
含义:
能量项链:
含义:区间合并
for(int i=2;i<2*n;i++){
for(int j=i-1;i-j<n&&j>=1;j--){//从i开始向前推
for(int k=j;k<i;k++)//k是项链的左右区间的划分点
s[j][i]=max(s[j][i],s[j][k]+s[k+1][i]+e[j]*e[k+1]*e[i+1]);
//状态转移方程:max(原来能量,左区间能量+右区间能量+合并后生成能量)
if(s[j][i]>maxn)maxn=s[j][i];//求最大值
}
}
248 G:
含义:
用f[i][j]表示将序列中的第i个数合并到第j个数全部合并所能得到的最大数值
if(f[i][pos] == f[pos+1][j])
{
f[i][j] = max(f[i][j], f[i][pos] + 1);
ans = max(ans, f[i][pos] + 1);
}
for(int len = 2; len <= n; len++) // 枚举所合并区间的长度,因为转移是从小区间转移到大区间,所以要从小到大枚举区间的长度
{
for(register int i = 1; i <= n - len + 1; i++) // 枚举区间的左端点 ,当左端点等于 全长 减去 区间长度 再加一 的时候,右端点取到序列的最右边
{
int j = i + len - 1;
for(register int pos = i; pos < j; pos++) // 枚举当前区间可以由哪两个小区间转移过来
{
if(f[i][pos] == f[pos+1][j] && f[i][pos] != 0 && f[pos+1][j] != 0) // 如果这两个小区间合并出的值相等,就进行转移 ,特别地,如果两个的值都是0,表示这两个区间什么也合并不出来,所以不能进行转移
{
f[i][j] = max(f[i][j], f[i][pos] + 1);
ans = max(ans, f[i][pos] + 1); // 用ans来保存答案
//cout<< f[i][pos] << ' ' << f[pos+1][j] << '\n'; 调试用
}
}
}
}
涂色:
含义:
for(int i=1;i<=n;++i)
f[i][i]=1;
for(int l=1;l<n;++l)
for(int i=1,j=1+l;j<=n;++i,++j) {
if(s[i]==s[j])
f[i][j]=min(f[i+1][j],f[i][j-1]);
else
for(int k=i;k<j;++k)
f[i][j]=min(f[i][j],f[i][k]+f[k+1][j]);
}
-
环形DP
-
树与图上的DP
树上的DP无法线性搜索,基本上只能通过DFS来实现,更加类似于记忆化搜索
常用转移方程形式为dp[i][j]
:以i为节点的子树上最大/小的值为
f[u][i]=max(f[u][i],f[u][i−j−1]+f[v][j]+e[i].w)
-
数位DP
常用状态转移方程
dp[i][j]
为:当前为第i位,前1位为j的情况总数
4. 状态压缩动态规划
特指用二进制来简化数组集合的动态规划
状态压缩通常使用二进制来存储状态信息 , 同样也利用二进制的快速运算的性质来加速程序运行.
将多维的状态用二进制表示
1.判断一个数字x二进制下第i位是不是等于1。
if ( ( ( 1 << ( i - 1 ) ) & x ) > 0)
将1左移i-1位,相当于制造了一个只有第i位上是1,其他位上都是0的二进制数。然后与x做与运算,如果结果>0,说明x第i位上是1,反之则是0。
2.将一个数字x二进制下第i位更改成1。
x = x | ( 1<<(i-1) )
证明方法与1类似,此处不再重复证明。
3.把一个数字二进制下最靠右的第一个1去掉。
x=x&(x-1)
1.取出x的第i位:
y = (x>>(i-1))&1;
2.将x第i位取反:
x ^= (1<<(i-1));
3.将x第i位变为1:
x |= (1<<(i-1));
4.将x第i位变为0:
x =x | (~(1<<(i-1)));
5.判断是否有两个连续的一:
if(x&(x<<1)) cout<<"YES";
6.枚举子集:
for( int x = sta ; x ; x = ( ( x - 1 )&sta) )
cout<<x;
李白喝酒:
逢店加一倍,遇花喝一斗
一路上,遇店5次,花10次,最后一次是花,此时刚好喝完
判断一共有多少种合法次序
解法
店为1,酒为0,枚举出14位二进制数的所有可能,并判断 判断一个二进制数中的某位是什么:10010&(1<<2)同1位1,否则为0
void test01()
{
int ans = 0;
for(int i = 0; i < (1 << 14); ++i){
int tot_1 = 0;
int tot_0 = 0;
int num = 2;
for(int j = 0; j < 14; ++j){
if(i & (1<<j)){ //判断第j+1位
tot_1++;
num *= 2;
}else{ //花
tot_0++;
num -= 1;
}
}
if(tot_1 == 5 && tot_0 == 9 && num == 1){
ans++;
}
}
cout << ans << endl;
}
状态压缩DP
若元素数量比较少,想要存储每个元素取或不取的状态,可用位运算进行压缩
空间复杂度为是2^n,这样开的空间很大,需要5维空间,但是位运算不需要
例题: n个人传递物品,编号1-n
开始时物品可以在任何一个人手上,再传给没给过的任意一个人 物品只能经过一个人一次,每个人有一个代价,人与人之间代价不同
经过n个人后,总代价【可以看成一个邻接矩阵】
解法
二进制位运算表示情况:
i | 1<<k把第k个人加入;
1 & 1<<j判断是否在集合中转移方程:
dp[i | 1 << k ][k] = min(dp[ i | 1<< k ][k], dp[i][j]+a[j][k]);
i |1<<k和i为位运算集合,k、j表示加入该位,下一个交给该位
我们把dp[1<<i][i]置为0,其余INF,
//dp[010000][i]=0表示从当前位置开始
//遍历整个数位0的位置,可以传递给他,再变成1
void test01()
{
int n;
cin >> n;
for(int i = 0; i < n; ++i){
for(int j = 0; j < n; ++j){
cin >> a[i][j];
}
}
memset(dp, 0x3f, sizeof(dp));
for(int i = 0; i < n; ++i){
dp[1<<i][i] = 0; //初始化,物品经过i位个人的手,现在在i手上
}
for(int i = 0; i < (1 << n); ++i){ //遍历全部可能 复杂度【2^n】
for(int j = 0; j < n; ++j){ //物品在谁手上,j 复杂度【n】
if(i & (1<<j)){ //i和j状态是否矛盾,需要在内
for(int k = 0; k < n; ++k){ //传递给k 复杂度【n】
if(!(1 & (1<<k))){ //k位不在i集合中中 ,k若已经在内了,不可重复加入
//核心,状态转移方程:加入k和不加入k
dp[i | 1<<k][k] = min(dp[i | 1<<k][k], dp[i][j]+a[j][k]);
}
}
}
}
}
int ans = N;
for(int i = 0; i < n; ++i){
ans = min(ans, dp[(1<<n)-1][i]);
}
cout << ans << endl;
}
二进制枚举
//有n个互不相同的数,从其中无重复选任意个,加法得到X
//求有多少种方案
using namespace std;
//6 6
//1 2 3 4 5 6
void test01()
{
int n, x, ans = 0, a[30];
cin >> n >> x;
for(int i = 0; i < n; ++i){
cin >> a[i];
}
//枚举2^n种可能
for(int i = 0; i < (1<<n); ++i){
int num = 0;
for(int j = 0; j < n; ++j){
if(i&(1<<j)){
num += a[j];
}
}
if(num == x){
ans++;
}
}
cout << ans << endl;
}
TSP问题
旅行商问题TSP ;哈密顿回路
所有地方只经过一次,最小代价
代码中间有部分用Floyd算法处理,用于解决非哈密顿回路时的问题,即不必只经过一次
若不需要只访问一次,可以用Floyd算法先计算两点最短路,称为闭包传递
//n个城市,只访问一次,最少代价
//路线是个环,从哪里开始都等价
//d(S, i):S:已经访问的城市的集合,i:当前城市
using namespace std;
//3
//-1 1 10
//1 -1 2
//10 2 -1
const int INF = 0x3f3f3f3f;
int dist[20][20];
int dp[1<<16][20];
void test01()
{
int n;
cin >> n;
//初始化
for(int i = 0; i < n; ++i){
for(int j = 0; j < n; ++j){
cin >> dist[i][j];
if(dist[i][j] == -1){ //-1表示无可到达,INF
dist[i][j] = INF;
}
}
}
// Floyd算法更新两点之间最短路,用于不唯一题解
// for(int k = 0; k < n; ++k){ //k必须在最外层
// for(int i = 0; i < n; ++i){
// for(int j = 0; j < n; ++j){
// dist[i][j] = min(dist[i][j], dist[i][k] + dist[k][j]);
// }
// }
// }
memset(dp, 0x3f, sizeof(dp));
dp[1][0] = 0;//起点
//从小到大遍历集合,必须满足所在城市在集合中,即s & (1<<i) != 0
for(int s = 0; s < (1<<n); ++s){ //所有可能
for(int i = 0; i < n; ++i){ //当前所在的城市
if(s & (1<<i)){ //!= 0确认当前所在城市在该集合中
for(int j = 0; j < n; ++j){ //上一步在哪个城市
if(j != i && (s & (1<<j))){ //两个城市不同且上一个城市也在集合中
dp[s][i] = min(dp[s][i], dp[s ^ (1 << i)][j] + dist[j][i]); //^异或:相当于减法,去掉城市j
}
}
}
}
}
int ans = INF;
for(int i = 0; i < n; ++i){ //返回起点0
ans = min(ans, dp[(1<<n)-1][i] + dist[i][0]);
}
if(ans == INF){ //无解
ans = -1;
}
cout << ans << endl;
}
方格取数
选定一个n*m的矩阵,行数和列数都不超过20,有些格子可以选,有些不可以;
选尽可能多的格子,保证所有格子没有公共边,如下所示:【可选位置标为1,不可选为0】
111 -> x1x
010 -> 0x0
则1111
1111
1111能选多少呢
//二维状态压缩:
// 设上一行状态为now,下一行为prev,只需要确保上下两个状态里没有重复元素,即now & prev = 0
// 此外,还需要保证选中的是允许选择的点,用压缩数组flag存储,即:now | flag == flag;确保不要出现0变1的情况,now应为flag的子集
// 同时,行内不能相连,即now & (now>>1) == 0成立,错位计算;第一位变为0,不影响
//2 3
//1 1 1
//0 1 0
using namespace std;
int a[20][20]; //存储输入数据
int state[21]; //标志
int dp[21][1<<20];
bool ok(int now){ //判断行内相交情况
return (now & (now>>1) == 0);
}
bool fit(int now, int i){ //判断now与输入所在行的匹配
return (now | state[i]) == state[i];
}
bool not_intersect(int now, int prev){ //判断相邻行间是否有交集边
return (now & prev == 0);
}
int count(int now){
int s = 0;
while(now){ //计算选取个数
s += now & 1;
now = now >> 1;
}
return s;
}
void test01()
{
int n, m;
cin >> n >> m;
for(int i = 0; i < n; ++i){
for(int j = 0; j < m; ++j){
cin >> a[i][j];
}
}
//将输入转化为二进制数
for(int i = 0; i < n; ++i){
for(int j = 0; j < m; ++j){
if(a[i][j]){
state[i+1] += 1 << j;
}
}
}
for(int i = 1; i <= n; ++i){ //行
for(int j = 0; j < (1 << m); ++j){ //所有状态枚举
if(ok(j) && fit(j, i)){ //行上符合且与原状态符合
for(int k = 0; k < (1 << m); ++k){ //上一行状态
if(ok(k) && fit(k, i-1) && not_intersect(j, k)){ //全部满足条件
dp[i][j] = max(dp[i][j], dp[i-1][k] + count(j));
}
}
}
}
}
int ans = 0;
for(int i = 0; i < (1<<m); ++i){ //枚举最后一行到底取了多少
ans = max(ans, dp[n][i]);
}
cout << ans << endl;
}
二维状态压缩:
设上一行状态为now,下一行为prev,只需要确保上下两个状态里没有重复元素,即now & prev = 0
此外,还需要保证选中的是允许选择的点,用压缩数组flag存储,即:now | flag== flag;确保不要出现0变1的情况,now应为flag的子集同时,行内不能相连,即now & (now>>1) == 0成立,错位计算;第一位变为0,不影响
5. 贪心算法
http://rbook.roj.ac.cn/article/5653c8f16c
动态规划和贪心比较迷,比较看感觉;
很多问题贪心都是错的,动态规划要做很多题才能知道怎么找状态转移方程
方格取数
例题:有一个 3×3
的方格,每个格子有一个正整数,要求从每行格子中取一个数,使得取出来的 3 个数字之和最大。
分析: 很简单,只要每一行取最大的数相加,最后的结构一定是最大.
这应该是最简单的贪心算法了,我们可以看到贪心算法的一个重要性质: 局部最优可以推出全局最优 , 如果一个问题不具有这种性质,就不可以使用贪心算法来解决.
代码:
#include <stdio>
int max =-1;
int tmp;
int sum=0;
int main(){
int i,j;
int n,m;
scanf("%d",&n,&m);
for (i=1;i<=n;i++){
max =-1;
for (j=1;j<=m;j++){
scanf("%d",&tmp);
if(max < tmp)
max=tmp;
}
sum+=max;
}
/* 输出结果 */
printf("%d",sum);
return 0;
}
最优装载问题
(1). 问题
给 n 个物体,第 i 个物体重量为 wi,选择尽量多的物体,使得总重量不超过 C。
(2). 分析
分析:由于只关心物体的数量,所以装重的没有装轻的划算。只需把所有物体按重量从小到大排序,依次选择每个物体,直到装不下为止。这就是一种典型的贪心算法,它只顾眼前,但却能得到最优解。
(3). 贪心策略:
先拿轻的
部分背包问题
(1). 问题: 有 n 个物体,第 i 个物体的重量为 wi,价值为 vi,在总重量不超过 C 的情况下让总价值尽量高。每一个物体可以只取走一部分,价值和重量按比例计算。
(2). 分析 分析:由于每一个物体可以只装入一部分。因此,物体的价值 / 重量越大,装入的总价值就越大。这个局部的最优策略,能满足全局最优,可以用此贪心法解答。
(3). 贪心策略: 先拿性价比高的
乘船问题
(1). 问题: 有 n 个人,第 i 个人重量为 wi。每艘船的载重量均为 C,最多乘两个人。用最少的船装载所有人。
(2). 分析 分析:首先考虑最重的人 i,他应该和谁一起坐呢?如果最轻的人都无法和他一起坐船,则唯一的方案就是他一个人坐一艘船。否则,他应该选择能和他一起坐船的人中最轻的一个 j。这样的方法是贪心的,因此它只是让 “眼前” 的浪费最少。
(3). 贪心策略: 最轻的人和最重的人配对。
总结
通常要求我们解决最优问题,有一个分割子问题的过程
如果是涉及到排队的问题,需要找一个元素,证明把它放在最前面最好,从交换最后两个元素开始想(本质是只有两个元素的时候)
- 第一步:题目是否是贪心:贪心的时间复杂度通常是 O(n)
- 第二步:熟悉题目的性质,找几个简单的样例来尝试
- 第三步:提出:如何才能最xxx,局部最优,对整体的贡献最少 / 最大
第四步:写代码
第五步:写对拍
核心是第二、三步,多写题目都能学会
搜索问题DFS和BFS考察最多,变化也最多
6. 深度优先搜索DFS
一般用于暴力求解解的个数
TMD,实在想不出来就骗分
英语:Depth-First-Search,DFS
是一种用于遍历或搜索树或图的算法。沿着树的深度遍历树的节点,尽可能深的搜索树的分支。当节点 v 的所在边都己被探寻过,搜索将回溯到发现节点 v 的那条边的起始节点。这一过程一直进行到已发现从源节点可达的所有节点为止。如果还存在未被发现的节点,则选择其中一个作为源节点并重复以上过程,整个进程反复进行直到所有节点都被访问为止。属于盲目搜索。
模板
void dfs( state arguments){
//递归出口
if(到达边界条件)
结束函数
for(i=0;i<=max;i++){ //枚举下一层的状态
state_next = opt
dfs(state_next)
}
}
回溯
global state; //全局状态
void dfs(){
if(到达边界条件)
结束函数
for(i=0;i<=max;i++){
state_next = opt
dfs(state_next)
unset state_next // 撤消状态的修改
}
}
- 迷宫是否有解
设置vis数组记录是否走过
如果下一个格子能走就往下走
走过的格子的不能走
如果 4 个方向的可能性都尝试过了且都不能走:就回溯
bool dfs(int x,int y){
visted[x][y] =1; //设这个点走过
//递归出口
if(x == zx && y == zy) //表示已经到了终点
return true;
int i;
for(i=0;i<4;i++){
//四种DFS方式
int tx = x+fx[i][0];
int ty = y+fx[i][1]; //tx,ty 下一个格子的坐标
//判断tx,ty 是不是可以走的格子
if( in_mg(tx,ty) && mg[tx][ty]!=1 && visted[tx][ty] !=1){
if( dfs(tx,ty) == true)
return true;//只要返回true 这层函数就结束了
// 上层函数也会接着返回true
// 整个递归就会不停的回溯
}
}
return false;
}
- 找到全部可能路径
核心: 你在回溯退出一个点的时候把这个设为未访问
因为这个点也许可以在走另一条路的时候走过
//调用了 dfs 相当于进入了x,y这个点
bool dfs(int x,int y){
visted[x][y] =1; //设这个点走过
if(x == zx && y == zy) //表示已经到了终点
{
cnt++;
return true;
}
int i;
for(i=0;i<4;i++){
int tx = x+fx[i][0];
int ty = y+fx[i][1]; //tx,ty 下一个格子的坐标
//判断tx,ty 是不是可以走的格子
if( in_mg(tx,ty) && mg[tx][ty]!=1 && visted[tx][ty] !=1){
dfs(tx,ty);
visted[tx][ty] = 0; //回溯时,把这个点设为 未访问
}
}
return false;
}
全排列和排列组合问题
void dfs(int d){
int i;
if( d == n+1){
printf("%4d:",++cnt);
for(i=1;i<=n;++i) printf("%d ",choose[i]);
printf("\n");
return;
}
for(i=1;i<=n;++i){
if( vis[i]) continue;
choose[d] = a[i]; //选
vis[i] = 1; // 标记选
dfs(d+1);
vis[i] = 0; //取消标记
}
}
※next_permutation
注意这个接口是从当前的字典序开始排序的,全排列需要先排序后排列
next_permutation(b+1,b+1+n);
n个数中选m个数进行全排列
void dfs(int d,int m){
int i;
if( d == m+1){
printf("%4d:",++cnt);
for(i=1;i<=m;++i) printf("%d ",choose[i]);
printf("\n");
return;
}
for(i=1;i<=n;++i){
if( vis[i]) continue;
choose[d] = a[i]; //选
vis[i] = 1; // 标记选
dfs(d+1,m);
vis[i] = 0; //取消标记
}
}
n 个数选 m 个数的组合
void dfs(int d,int pre,int m){
int i,j;
if( d == m+1) { //选完了
for(i=1;i<=m;++i) printf("%d ",choose[i]);
printf("\n");
return;
}
for(i=pre+1;i<=n;++i){//组合数无顺序,后面不需要考虑前面的数
choose[d] = a[i];
dfs(d+1,i,m);
}
}
二进制优化(类似背包的二进制优化)
int main(){
scanf("%d",&n);
int i,j;
for(i=1;i<=n;++i) scanf("%d",&a[i]);
for(i=0;i<(1<<n);i++){
for(j=0;j<n;j++){ //枚举位置
if( i & (1<<j) ) printf("%d ",j+1);
}
printf("\n");
}
return 0;
}
深度优先搜索剪枝
要求正确、准确、高效
下面的5点中,需要特别注意的是记忆化搜索
记忆化搜索在大数据量的时候很有用,主要看自己感觉
优化搜索顺序
如果要求深度最低的叶子结点,明显先向左边走,先能达到最值点 2
排除等效冗余
在搜索的过程中,如果我们能够判定从搜索树的当前解都会沿着几条不同的分支到达的子树是等效的,那么只要对其中的一条分支进行搜索.
可行性剪枝
当搜索到某一个点 (状态) 时,及时对当前点进行检查,如果发现分支已经不可能达到递归边界了 (得到答案), 就回溯.
发现当前路径不可能达到终点,那就回溯.
特别的当某个点被限制在一个区间内的时候,此时可行性剪枝被称为上下界剪枝
最优性剪枝
在搜索的过程中,如果当前花费的代价已经超过已经得到的解中的最优解 , 无论如何如当前点出发都不可能更新答案了 ! 执行回溯.
如图 1, 如果要求深度最低的叶子结点,明显先向左边走,先能达到最值点 2
, 随后只要超过点 2深度的深度,就回溯
记忆化
已经搜索过的状态,记录下来,下一次遇到这个状态是,直接返回结果。是 DP 的搜索写法.
※记忆化搜索
https://blog.csdn.net/u010700335/article/details/44136339
我们在求动态规划的时候,利用状态转移方程,不停的从一个已知的状态推出一个未知的状态 , 每一个状态都相当于一个子问题 , 每一个子问题都只计算一遍,这也就是动态规划比搜索快的原因:解决了重复子问题的计算.
我们可以这种过程抽象成一种拓扑关系 , 每一个状态都是从已计算的前趋状态得到的。但是如果这种拓扑关系很复杂,很难得到一个状态的前趋状态 ,
应该怎么办?
这种情况的难点在于: 计算前趋状态 , 如果能避免这种计算 , 那问题就迎刃而解.
搜索的优点在于:只计算后趋 , 算法过程简单,但是会产生大量的重复子问题
DP 的优点在于:不会计算 重复子问题 , 但是面对复杂的前趋,没有优势搜索和 DP 的结合,综合的两者的有点: 记忆化搜索
记忆化搜索的关键就是在搜索过程中能够将某个状态确定的值求出来,当未来再次访问该状态时,可以直接return
记忆化搜搜关键:dp[x][y] = max(dfs(dx, dy) + 1, dp[x][y]);
记忆化搜索编写bfs函数时,结合dp数组的状态设置一起设置参数,如dfs(i,j) = dfs(i-1, j) + dfs(i, j-1);
来自
//滑雪
#include <stdio.h>
#include <algorithm>
using namespace std;
int map[101][101];
int dp[101][101];
int dir[4][2] = {1, 0, 0, -1, -1, 0, 0, 1};
int r, c;
int dfs(int x, int y)
{
if (dp[x][y])
return dp[x][y];
for (int i = 0; i < 4; i++)
{
int dx = x + dir[i][0], dy = y + dir[i][1];
if (dx > 0 && dx <= r && dy > 0 && dy <= c && map[dx][dy] < map[x][y])
{
dp[x][y] = max(dfs(dx, dy) + 1, dp[x][y]);
}
}
return dp[x][y];
}
int main()
{
scanf("%d %d", &r, &c);
for (int i = 1; i <= r; i++)
{
for (int j = 1; j <= c; j++)
{
scanf("%d", &map[i][j]);
}
}
int len = 0;
for (int i = 1; i <= r; i++)
{
for (int j = 1; j <= c; j++)
{
len = max(dfs(i, j), len);
}
}
printf("%d\n", len + 1);
return 0;
}
**A*算法(**启发式算法)
还没有学习,目前也没有做过
7. ※广度优先搜索BFS
广泛应用于求解最优解,最小值
详情见k短路
特点
就是这些点按层级的顺序访问。因为这个特点,我们可以使用 bfs 来求一些有关,最短路径,最小步骤等相关问题.
寻找迷宫的最短解
int bfs(int x,int y){
q.push({x,y,1});
vis[x][y] = 1;
while (!q.empty()) {
node h = q.front();
if( h.x == tx && h.y == ty) return h.s;
q.pop();
for(int i=0;i<=3;++i){
// nx = new x
int nx = h.x + fx[i][0];
int ny = h.y + fx[i][1];
if( maze[nx][ny] == 0 && in_mg(nx,ny) && vis[nx][ny] == 0){
q.push({nx,ny,h.s+1});
vis[nx][ny] = 1; //设这个点走过
}
}
}
return -1;
}
双向BFS
双向bfs适用于知道起点和终点的状态下使用,从起点和终点两个方向开始进行搜索,可以非常大的提高单个bfs的搜索效率
同样,实现也是通过队列的方式,可以设置两个队列,一个队列保存从起点开始搜索的状态,另一个队列用来保存从终点开始搜索的状态,如果某一个状态下出现相交的情况,那么就出现了答案
双向BFS求解迷宫最短解法
感觉好像也没见过双向BFS的题目
一般有想法BFS的话还是BFS,能解大部分解
思路
整体思路还是基于BFS的,就是从两边同时进行BFS,然后扫描中间的状态,相等时即为答案
代码思路
- 开辟两个队列,将头尾节点放入
- 将从起点经过的点标记为1,从终点经过的点标记为2
- 在两边的水不断漫过过程中,出现碰撞就return
- 几个注意点
while(!q1.empty() || !q2.empty())
两者必须同时满足,否则会有双向无法到达- 分两次进行bfs,一次
if(!q1.empty()){bfs1}
,再一次if(!q2.empty()){bfs2}
- 出口,当染色时出现颜色不相同时,将两个位置的值相加
#include <iostream>
#include <queue>
#define P pair<int, int>
using namespace std;
//记录下当前状态, 从前往后搜索值为1,从后往前搜索值为2,如果某状态下,当前节点和准备扩展节点的状态相加为3,说明相遇
queue <P> q1, q2;
int r, c, ans, dis[45][45], vst[45][45];
int dx[4] = {-1, 0, 1, 0};
int dy[4] = {0, 1, 0, -1};
char m[45][45];
void dbfs() {
bool flag;
q1.push(P(1, 1)), dis[1][1] = 1, vst[1][1] = 1; //从前搜
q2.push(P(r, c)), dis[r][c] = 1, vst[r][c] = 2; //从后搜
while(!q1.empty() && !q2.empty()) {
int x0, y0;
if(q1.size() > q2.size()) { //每次扩展搜索树小的队列 flag=1扩展前搜的队列,flag=0扩展后搜的队列
x0 = q2.front().first, y0 = q2.front().second;
q2.pop();
flag = 0;
}else {
x0 = q1.front().first, y0 = q1.front().second;
q1.pop();
flag = 1;
}
for(int i = 0; i < 4; i++) {
int nx = x0 + dx[i];
int ny = y0 + dy[i];
if(nx >= 1 && nx <= r && ny >= 1 && ny <= c && m[nx][ny] == '.') {
if(!dis[nx][ny]) {
dis[nx][ny] = dis[x0][y0] + 1;
vst[nx][ny] = vst[x0][y0];
if(flag) q1.push(P(nx, ny));
else q2.push(P(nx, ny));
}else {
if(vst[x0][y0] + vst[nx][ny]== 3) { //相遇
ans = dis[nx][ny] + dis[x0][y0];
return;
}
}
}
}
}
}
int main() {
cin >> r >> c;
for(int i = 1; i <= r; i++)
for(int j = 1; j <= c; j++)
cin >> m[i][j];
dbfs();
cout << ans << "\n";
return 0;
}
看到了一个比较好的例题,代码也很好看
#include<iostream>
#include<queue>
using namespace std;
char ss[3];
char ee[3];
typedef struct node
{
int x;
int y;
int steps;
}node;
int d[8][2]={{-2,1},{-2,-1},{-1,-2},{-1,2},{2,-1},{2,1},{1,-2},{1,2}};
int visited[8][8];
int color[8][8];//区分当前位置是哪个队列查找过了
node s;
node e;
int in(node n)
{
if(n.x<0||n.y<0||n.x>7||n.y>7)
return 0;
return 1;
}
int bfs()
{
queue<node>qf; //我发现如果把qf和qb放在外面的话,节省的时间挺惊人的,耗时16MS
queue<node>qb;
memset(visited,0,sizeof(visited));
memset(color,0,sizeof(color));
qf.push(s);//存入起点
qb.push(e);//存入终点
//visited数组存放双向BFS的步骤数目
visited[s.x][s.y]=0;//起点从0步骤开始
visited[e.x][e.y]=1;//终点自带1个步骤
color[s.x][s.y]=1;//着色(正向)
color[e.x][e.y]=2;//着色(反向)
while(!qf.empty()||!qb.empty())
{
if(!qf.empty())
{
node st=qf.front();
qf.pop();
for(int i=0;i<8;++i)
{
node t;
t.x=st.x+d[i][0];
t.y=st.y+d[i][1];
if(in(t))
{
if(color[t.x][t.y]==0){//都还没有走过
visited[t.x][t.y]=visited[st.x][st.y]+1;
color[t.x][t.y]=1;
qf.push(t);
}
else if(color[t.x][t.y]==2){//在反相BFS中已经走过了
return visited[st.x][st.y]+visited[t.x][t.y];
}
}
}
}
//两个队列的遍历同时进行
//正向走一步,反向走一步,对称操作,防止互相错过
if(!qb.empty())
{
node st=qb.front();
qb.pop();
for(int i=0;i<8;++i)
{
node t;
t.x=st.x+d[i][0];
t.y=st.y+d[i][1];
if(in(t))
{
if(color[t.x][t.y]==0){
visited[t.x][t.y]=visited[st.x][st.y]+1;
color[t.x][t.y]=2;
qb.push(t);
}
else if(color[t.x][t.y]==1){
return visited[st.x][st.y]+visited[t.x][t.y];
}
}
}
}
}
}
int main(int argc, char *argv[])
{
// freopen("in.txt","r",stdin);
while(scanf("%s %s",ss,ee)==2)
{
s.x=ss[0]-'a';
s.y=ss[1]-'1';
e.x=ee[0]-'a';
e.y=ee[1]-'1';
s.steps=0;
e.steps=1;
if(s.x==e.x&&s.y==e.y)
printf("To get from %s to %s takes 0 knight moves.\n",ss,ee);
else
printf("To get from %s to %s takes %d knight moves.\n",ss,ee,bfs());
}
return 0;
}
01BFS
背景
边权值为0或1,或者能够转化为这种边权值的最短路问题,时间复杂度O( v点+e边 )。
主要操作:用deque,从0边扩展到的点push到队首,反之则到队尾。每次从队首取点,直到队列为空
原理解析
我们以水的蔓延来理解 BFS
想象有一个时钟,在计时。
计时的单位为一。每过一个时刻。水流就会向周围的点蔓延。
当水流到达这个点的时刻。就是最短路.
为什么我们要采用队列呢?
很明显,队列头部的点要比后边的点。花费的时间要少 (小于等于) 那么从时间上来考虑,一定是队列头部的点先进行活动
所以,队列的本质就是对点的优先级进行排列。优先级高的点,先活动.
我们可以把这种思想带入到 01BFS上。
代码思路
采用双端队列deque
从0边扩展到的点放队首
从1边扩展到的点放队尾
每次从队首取元素,直到队空
代码模板
void _01bfs(int state,int sx,int sy){
/* 清空标记 */
memset(vis,0,sizeof(vis));
typedef struct {
int x,y,step;
} node;
deque<node> q;
q.push_back({sx,sy,0}); //加入起点
while(!q.empty()){
node h = q.front();
q.pop_front();
if(vis[h.x][h.y])
continue;
vis[h.x][h.y] = 1; //标记
dis[state][h.x][h.y] = h.step;
int i;
for(i = 0; i < 4;i++ ){
int nx = h.x + fx[i][0];
int ny = h.y + fx[i][1];
if( in_map(nx,ny) && !vis[nx][ny] && _map[x][y] != '#' ){
if( _map[x][y] == '0')
q.push_front({nx,ny,h.step});
else
q.push_back({nx,ny,h.step+1})
}
}
}
}
模板题:SPOJ - KATHTHI
题意:起点走到终点,n×m的网格,每个位置有一个小写字母,若s[x][y]=s[nx][ny],则移动的花费为0,否则花费为1,求花费最少
0放队首,1放队尾
#include <bits/stdc++.h>
#define debug freopen("r.txt","r",stdin)
#define mp make_pair
#define ri register int
using namespace std;
typedef long long ll;
typedef pair<int, int> pii;
const int maxn = 2e3+100;
const int INF = 0x3f3f3f3f;
const int mod = 998244353;
const int dx[]={0,1,0,-1};
const int dy[]={1,0,-1,0};
inline ll read(){ll s=0,w=1;char ch=getchar();
while(ch<'0'||ch>'9'){if(ch=='-')w=-1;ch=getchar();}
while(ch>='0'&&ch<='9') s=s*10+ch-'0',ch=getchar();
return s*w;}
ll qpow(ll p,ll q){return (q&1?p:1)*(q?qpow(p*p%mod,q/2):1)%mod;}
struct node
{
int x,y;
};
node s,t;
int n,m,k,T;
char G[maxn][maxn];
struct _01bfs
{
deque<node> q;
int dis[maxn][maxn];
int x,y,xx,yy;
bool vis[maxn][maxn];
node now;
bool flag;
void init()
{
memset(dis,0x3f,sizeof(dis));
//memset(vis,0,sizeof(vis));
q.clear();
}
int bfs()
{
q.push_front(s);
dis[s.x][s.y]=0;
while (!q.empty())
{
now=q.front();
q.pop_front();
//if (vis[now.x][now.y]) continue; 一般情况下,可以不用
//vis[now.x][now.y]=1;
for (int i=0;i<4;i++)
{
x=now.x,y=now.y;
xx=x+dx[i],yy=y+dy[i];
if(xx<0||xx>=n||yy<0||yy>=m) continue;
flag=1;
if(G[x][y] == G[xx][yy]) flag=0; //边权为1
//松弛
if (dis[x][y]+flag<dis[xx][yy]) // 如果是<=就要用vis
{
dis[xx][yy]=dis[x][y]+flag;
if (!flag) q.push_front({xx,yy});//边权为0
else q.push_back({xx,yy});//边权为1
}
}
}
return dis[t.x][t.y];
}
}hehehe;
int main()
{
T=read();
while (T--)
{
n=read(),m=read();
for (int i=0;i<n;i++) scanf("%s",G[i]);
s.x=0,s.y=0,t.x=n-1,t.y=m-1;
hehehe.init();
cout<<hehehe.bfs()<<endl;
}
return 0;
}
二叉树遍历,树的直径,树的重心
https://blog.csdn.net/zhanxufeng/article/details/80715185
#include <cstdio>
#include <cstring>
char a[20],b[20];// a中序 b后序
void dfs(char a[],char b[],int len){
int i = 0;
char root = a[0];
while(root != b[i]) i++; //找出中序中根的位置
if( i >0)
dfs(a+1,b,i); //遍历左子树
if( i < len -1)
dfs(a+i+1,b+i+1,len-1-i);//遍历右子树
printf("%c",root);//输出根
}
int main(){
scanf("%s%s",a,b);
dfs(a,b,strlen(b));
return 0;
}
二叉树的遍历
#include<iostream>
using namespace std;
struct Node
{
char data; // data代表节点的数字(内容)
Node* L; // L,R 又分别为左右节点
Node* R;
};
Node* T; // 创建一个指针T 指向这个二叉树的首地址(即这个二叉树的根节点)
// ================先创建一个二叉树 //
void creatNode(Node* &T)
{
char ch;
if ((ch = getchar()) == '#') // 按照一个二叉树的前序遍历输入若子节点为空就输入 #
{
T = NULL;
}
else
{
T = new Node;
T->data = ch;
creatNode(T->L);
creatNode(T->R);
}
}
// ================完成二叉树的创建 //
// ================前序遍历 //
void preOrder(Node* &T)
{
if (T == NULL)//如果二叉树为空直接返回结束程序,下同
return;
else
{
cout << T->data << " "; //先输出根节点的那个数字
preOrder(T->L); //然后访问左孩子,开始递归
preOrder(T->R); //左孩子访问完成,开始右孩子递归
}
}
// ================中序遍历 //
void inOrder(Node* &T)
{
if (T == NULL)
return;
else
{
inOrder(T->L);
cout << T->data << " ";
inOrder(T->R);
}
}
// ================后序遍历 //
void posOrder(Node* &T)
{
if (T == NULL)
return;
else
{
posOrder(T->L);
posOrder(T->R);
cout << T->data << " ";
}
}
int main()
{
cout << "输入一个二叉树(按照这个二叉树的前序遍历输入,若子节点为空就输入 # ):" << endl;
creatNode(T);
cout << "前序遍历:";
preOrder(T);
cout << endl;
cout << "中序遍历:";
inOrder(T);
cout << endl;
cout << "后序遍历:";
posOrder(T);
cout << endl;
delete T;
return 0;
}
树的直径问题(两次DFS)
代码思路
- 先从某一点开始进行一次BFS or DFS,找到最远的一个点;
- 换元状态,更新状态;
- 从找到的最远的点开始,再进行一次BFS or DFS,此时的距离就是树的直径。
//大臣的旅费
//树的直径问题
const int maxn = 10010;
int n, cost, restart;
bool vis[maxn];
struct Node{
int v;
int cost;
Node(int _v, int _cost){
v = _v;
cost = _cost;
}
};
vector<Node> G[maxn];
void dfs(int node, int cur_cost){
if(cur_cost > cost){
restart = node; //新起点
cost = cur_cost; //最大长度
}
for(int i = 0; i < G[node].size(); ++i){
Node cur = G[node][i];
if(!vis[cur.v]){
vis[cur.v] = 1;
dfs(cur.v, cur_cost + cur.cost);
vis[cur.v] = 0;
}
}
}
void test01()
{
int u, v, cos;
cin >> n;
for(int i = 1; i < n; ++i){
cin >> u >> v >> cos;
G[u].push_back(Node(v, cos));
G[v].push_back(Node(u, cos));//邻接表
}
//两次dfs, 得到restart点,从该店开始在此dfs
vis[1] = 1;
dfs(1, 0);
vis[1] = 0;
//恢复参数
memset(vis, 0, sizeof(vis));
cost = 0;
vis[restart] = 1;
dfs(restart, 0);
//得到相距最远的点的距离cost
// cout << cost << endl;
cout << cost*10 + (cost+1)*cost/2 << endl;
}
int main()
{
test01();
return 0;
}
树的重心(难)
定义
对于树上的每一个点,计算其所有子树中最大的子树节点数,这个值最小的点就是这棵树的重心。
(这里以及下文中的“子树”都是指无根树的子树,即包括“向上”的那棵子树,并且不包括整棵树自身。)
性质
以树的重心为根时,所有子树的大小都不超过整棵树大小的一半。
树中所有点到某个点的距离和中,到重心的距离和是最小的;如果有两个重心,那么到它们的距离和一样。
把两棵树通过一条边相连得到一棵新的树,那么新的树的重心在连接原来两棵树的重心的路径上。
在一棵树上添加或删除一个叶子,那么它的重心最多只移动一条边的距离。
求法
在 DFS 中计算每个子树的大小,记录“向下”的子树的最大大小,利用总点数 - 当前子树(这里的子树指有根树的子树)的大小得到“向上”的子树的大小,然后就可以依据定义找到重心了。
https://blog.csdn.net/weixin_43810158/article/details/88391828
https://blog.csdn.net/acdreamers/article/details/16905653
LCA(tarjan和树上倍增)
树上倍增
代码思路
- 树上倍增主要用来解决最近公共祖先问题
- 建立邻接表存储边和点;
ll depth[maxn]
; //节点i的深度
ll fa[maxn][22]
; //表示节点i上面第2^k层的父节点,这是倍增的体现- DFS更新所有点的倍增父节点
- dfs(son, dad);//递归
depth[son] = depth[dad]+1
; //子节点深度为父节点深度+1fa[son][0] = dad
; //son近上一层是父亲fa[son][i] = fa[fa[son][i-1]
][i-1]; //第该节点的第2i个祖先为该节点2(i-1)祖先的第2^(i-1)个祖先- 遍历所有的儿子进行更新
//LCA-最近公共祖先
#include <iostream>
#include <ctime>
#include <vector>
#include <cstdlib>
#include <sstream>
#include <algorithm>
using namespace std;
typedef long long ll;
const ll maxn = 5e5+9;
ll depth[maxn]; //节点i的深度
ll fa[maxn][22]; //表示节点i上面第2^k层的父节点,这是倍增的体现
int lg[maxn]; //log(i)+1;
int tot; //?
vector<ll> v[maxn];
//创建邻接表
void add(ll x, ll y){
v[x].push_back(y);
v[y].push_back(x);
}
void dfs(ll son, ll dad){ //子节点、父节点
depth[son] = depth[dad]+1; //子节点深度为父节点深度+1
fa[son][0] = dad; //son上面一层
//核心,倍增的关键
for(int i = 1; (1<<i) <= depth[son]; ++i){ //找到2^i次方的父节点
fa[son][i] = fa[fa[son][i-1]][i-1]; //第该节点的第2^i个祖先为该节点2^(i-1)祖先的第2^(i-1)个祖先
}
//遍历所有的儿子
for(int i = 0; i < v[son].size(); ++i){
if(v[son][i] != dad){ //该子节点的第2^i祖先如果不是父亲的话,深搜
dfs(v[son][i], son); //以子节点的2^i祖先为孩子
}
}
}
int lca(int x, int y){
if(depth[x] < depth[y]){ //确保x比y更深
swap(x, y);
}
//x,y调整到同一高度
while(depth[x] != depth[y]){
x = fa[x][lg[depth[x]-depth[y]]-1];
}
if(x == y){
return x; //本来就是子孙关系 ,在一个链上,y是x的祖先
}
for(int k = lg[depth[x]]; k >= 0; k--){
if(fa[x][k] != fa[y][k]){
x = fa[x][k];
y = fa[y][k]; //
}
}
return fa[x][0]; //返回x的爸爸
}
int n, m, s;
int x, y;
void test01()
{
cin >> n >> m >> s;
for(int i = 1; i <= n; ++i){
lg[i] = lg[i-1];
if(i == 1<<lg[i-1]){
lg[i]++; //log2底
}
}
for(int i = 1; i <= n-1; ++i){
cin >> x >> y;
add(x, y);
add(y, x);
}
dfs(s, 0); //从树根开始dfs
for(int i = 1; i <= m; ++i){
cin >> x >> y;
cout << lca(x, y) << endl;
}
}
int main()
{
double start = clock();
test01();
double end = clock();
cout <<"程序用时" << end - start << "ms" << endl;
return 0;
}
tarjan
BLOG
不错的博客
tarjan算法主要用于证明图的强联通性以及割点、缩点操作
代码思路
- targan算法有两个非常关键的数组,low数组和dfn数组
- 初始化dfn和low数组,设置为相同值(节点编号cnt++)
- 入栈,作为栈顶元素,同时更新vis数组
- 遍历所有可能达到的点
- 如果该点没有访问过,深搜,并更新low
- 访问过
//tarjan算法
const int maxn = 1e5+9;
vector<int> v[maxn];
//tarjan算法基于dfs实现
int ans[maxn];
int vis[maxn];
int stack[maxn];
//关键数组
//当dfn[u] == low[u]时,我们可以判断一个强联通分量
int dfn[maxn];
int low[maxn];
int n, m, x, y;
int tt, cnt, sig;
//初始化
void init(){
memset(vis, 0, sizeof(vis));
memset(low, 0, sizeof(low));
memset(dfn, 0, sizeof(dfn));
}
//u结点开始的强联通分量
void tarjan(int u){
vis[u] = 1;
low[u] = dfn[u] = cnt++;
for(int i = 0; i < v[u].size(); ++i){
int tu = v[u][i];
if(vis[tu] == 0) tarjan(tu); //深搜
if(vis[tu] == 1) low[u] = min(low[u],low[tu]); //更新
}
if(dfn[u] == low[u]){
sig++; //强联通分量+1
//缩点代码
//????
//我们知道,Tarjan算法相当于在一个有向图中找有向环,缩点!!
//缩点基于一种染色实现,我们在Dfs的过程中,尝试把属于同一个强连通分量的点都染成一个颜色,
//那么同一个颜色的点,就相当于一个点。
do{
low[stack[tt]] = sig;
color[stack[tt]] = sig;
vis[stack[tt]] = -1;
} while(stack[tt--] != u)
}
}
void solve(){
//tt:栈顶
//cnt:计数,low[u] = bfn[u] = cnt++;
//sig:强连通分量数目
tt = -1; cnt = 1; sig = 0;
for(int i = 1; i <= n; ++i){
if(vis[i] == 0){
tarjan(i);
}
}
cout << sig << endl;
}
void test01()
{
cin >> n >> m; //n point m edge
init();
for(int i = 0; i < m; ++i){
cin >> x >> y;
v[x].push_back(y);
}
//求解强联通分量数量
solve();
}
几种路径算法的关系(BFS,01BFS,Dijkstra,A∗)
本质上用的都是同一种思想(水的蔓延)。
例如,BFS 是 01BFS 的一种特例,Dijkstra 是 A∗ 特例.
记忆化搜索
做动态规划时,我们利用状态转移方程,不停的从一个已知的状态推出一个未知的状态 , 每一个状态都相当于一个子问题。每一个子问题都只计算一遍,这也就是动态规划比搜索快的原因:解决了重复子问题的计算.
我们可以这种过程抽象成一种拓扑关系 , 每一个状态都是从已计算的前趋状态得到的。但是如果这种拓扑关系很复杂,很难得到一个状态的前趋状态 , 应该怎么办?
这种情况的难点在于: 计算前趋状态 , 如果能避免这种计算 , 那问题就迎刃而解.
搜索的优点在于:只计算后趋 , 算法过程简单,但是会产生大量的重复子问题
DP 的优点在于:不会计算 重复子问题 , 但是面对复杂的前趋,没有优势
搜索和 DP 的结合,综合的两者的有点: 记忆化搜索
简而言之
就是将计算过的子问题存在一个表中,在递归的过程中遇到计算过的子问题就返回
https://blog.csdn.net/u010700335/article/details/44136339
典型:记忆化递归菲波那切数列
LCA最近公共祖先-树上倍增算法
LCT
//Milk Visits S
//LCA
using namespace std;
typedef long long ll;
const ll maxn = 2e5+9;
ll depth[maxn];
int lg[maxn];
ll fa[maxn][22];
ll n;
vector<ll> v[maxn];
void add(int x, int y){
v[x].push_back(y);
v[y].push_back(x);
}
void dfs(ll son ,ll dad){
//深度更新
depth[son] = depth[dad]+1;
//父亲更新
fa[son][0] = dad;
//祖先更新
for(int i = 1; (1<<i) <= depth[son]; ++i){
fa[son][i] = fa[fa[son][i-1]][i-1];
}
//遍历所有的儿子,继续dfs
for(int i = 0; i < v[son].size(); ++i){
if(v[son][i] != dad){
dfs(v[son][i], son);
}
}
}
//树上倍增
ll lca(int x, int y){
//调整两个节点深度
if(depth[x] < depth[y]){
swap(x, y);
}
while(depth[x] != depth[y]){
x = fa[x][lg[depth[x]-depth[y]]-1]; //更新到同一高度
}
//本来就在一条链上
if(x == y){
return x;
}
//一起往上跳
for(ll k = lg[depth[x]]; k >= 0; --k){
if(fa[x][k] != fa[y][k]){
x = fa[x][k];
y = fa[y][k];
}
}
return fa[x][0];
}
void test01()
{
memset(depth, 0, sizeof(depth));
memset(fa, 0, sizeof(fa));
cin >> n;
int x, y;
for(int i = 1; i <= n; ++i){
cin >> x >> y;
add(x, y);
}
//建立对数数列
lg[0] = 0;
for(int i = 1; i <= n; ++i){
lg[i] = lg[i-1];
if(i == 1<<lg[i-1]){
lg[i]++;
}
}
dfs(1, 0);
cout << lca(3, 5) << endl;
}
8. 树与图:
图的存
邻接矩阵
void test01()
{
int G[6][6];
memset(G, 0, sizeof(G));
int m;
cin >> m;
//初始化邻接矩阵
for(int i = 0; i < m; ++i){
int a, b;
cin >> a >> b;
G[a][b] = 1; //这里也可以改成权值
}
for(int i = 1; i <= 5; ++i){
int sum = 0;
for(int j = 1; j <= 5; ++j){
if(G[i][j] == 1 && G[j][i] == 1){
sum++;
}
}
cout << i << "have" << sum << "friends" << endl;
}
}
邻接表
邻接表的普通实现
int n, m;
vector<int> G [maxn];
cin >> n >> m;
int u, v;
for(int = 1; i <= m; ++i){
cin >> u >> v;
G[u].push_back(v);//有向图
G[v].push_back(u);//无向图
}
邻接矩阵的链表实现(一般直接用vector实现也可以了)
//邻接表的链表实现
const int M = 10000000;
const int N = 10000;
struct edge{
int v, w, next;
}e[M];
int p[N], eid;
void init(){ //初始化必须在建图前完成
memset(p, -1, sizeof(p));
eid = 0;
}
void insert(int u, int v, int w){
e[eid].v = v;
e[eid].w = w;
e[eid].next = p[u];
p[u] = eid++;
}
void insert2(int u, int v, int w){
insert(u, v, w);
insert(v, u ,w);
}
void test01()
{
for(int i = 0; i < n; ++i){
for(int j = p[i]; ~j; j = e[j].next){
cout << i << "->" << e[j].v << "," << e[j].w << endl;
}
}
}
int main()
{
double start = clock();
test01();
double end = clock();
cout <<"程序用时" << end - start << "ms" << endl;
return 0;
}
重点
单源最短路径–迪杰斯特拉算法
dijkstra的代码暂时还没搞懂
dijkstra算法不能处理负权图
设起点为 s ,dis[i] 表示,起点 s 到点 i 的最短路径的长度,dijkstra 过程如下:
dis [s] = 0, 标记点 s
用已经标记的点去更新未标记的点
在未标记的点里找一个 dis 值最小的点,标记它
重复 2,3 两个步骤,直到的点都标记了,就得到了所有点的 dis 值
Dijstra 算法本质: 贪心
有两个点的集合: A 确定的最短路径的点,B 没有确定最短路径的点
每一次从 B 中找到 dis 最小的点 c, 把 c 加入 A
不停这样下去,直到所有点都成为 A
//实现dijkstra算法
#include <iostream>
#include <ctime>
#include <vector>
#include <cstdlib>
#include <sstream>
#include <algorithm>
#include <cstring>
using namespace std;
const int N = 1001;
const int M = 10001;
struct edge{
int v, w, next;
edge(){}
edge(int _v, int _w, int _next){
v = _v;
w = _w;
next = _next;
}
} e[M*2]; //存储了所有的节点
int head[N], size; //size边数, head[]为
void init(){
memset(head, -1, sizeof(head));
size = 0;
}
void insert(int u, int v, int w){
e[size] = edge(v,w,head[u]); //存一个新点
head[u] = size++;
}
void insert2(int u, int v, int w){
insert(u, v, w);
insert(v, u, w);
}
int n, m;
int dis[N];
int vis[N];
void dijkstra(int u){
memset(vis, false, sizeof(vis));
memset(dis, 0x3f, sizeof(dis));
dis[u] = 0;
for(int i = 0; i < n; ++i){//遍历所有的点
int mind = 100000000, minj = -1;
for(int j = 1; j <= m; ++j){ //遍历所有的边求出i到j边求出两点最短路径
// 寻找当前最小的路径;
// 即,在未获取最短路径的顶点中,找到离vs最近的顶点(k)。
if(!vis[j] && dis[j] < mind){
minj = j;
mind = dis[j];
}
}
if(minj == -1){ //不成图
return;
}
vis[minj] = true;
for(int j = head[minj]; j != -1; j = e[j].next){
int v = e[j].v;
int w = e[j].w;
if(!vis[v] && dis[v] > dis[minj]+w){
dis[v] = dis[minj] + w;
}
}
}
}
void test01()
{
init();
int u, v, w;
cin >> n >> m;
while(m--){
cin >> u >> v >> w;
insert2(u, v, w);
}
dijkstra(1);
cout << dis[n] << endl;
}
int main()
{
double start = clock();
test01();
double end = clock();
cout <<"程序用时" << end - start << "ms" << endl;
return 0;
}
dijkstra最基础的版本,时间复杂度O(2n2)
dist数组:初始点到其余各个顶点的最短路径;
vis数组:下标和dist数组对应;
<1>若值为false,则该点为未确定点,dist数组里的对应值是未确定的;
<2>若值为true,则该点为已确定点,dis数组里的对应值是确定的;
算法基本思路:每次找到离源点(我们目前指定的初始点就是1号顶点)最近的一个顶点,然后以该顶点为中心进行扩展,
不断更新源点到其余所有点的最短路径。
#include<bits/stdc++.h>
using namespace std;
const int INF=0x3f3f3f3f;
const int maxSize=1e3+5;
int n,m,e[maxSize][maxSize],dist[maxSize];
void dijkstra(int x)
{
int vis[maxSize],i,j,u,minn;
for(i=1;i<=n;i++)
{
dist[i]=e[x][i];
vis[i]=0;
}
vis[x]=1;
for(i=1;i<n;i++)
{
minn=INF;
for(j=1;j<=n;j++)
if(!vis[j]&&dist[j]<minn)
{
u=j;
minn=dist[j];
}
vis[u]=1;
for(j=1;j<=n;j++)
if(!vis[j]&&dist[j]>dist[u]+e[u][j])
dist[j]=dist[u]+e[u][j];
}
}
int main()
{
int i,j,u,v,w,x;
while(cin>>n>>m)
{
for(i=1;i<=n;i++)
for(j=1;j<=n;j++)
if(i==j)e[i][j]=0;
else e[i][j]=INF;
for(i=1;i<=m;i++)
{
scanf("%d%d%d",&u,&v,&w);
e[u][v]=w;
}
cin>>x;
dijkstra(x);
for(i=1;i<=n;i++)
cout<<dist[i]<<" ";
}
return 0;
}
vector邻接表版本
#include<bits/stdc++.h>
using namespace std;
#define MAX 10005
#define INF 0x3f3f3f3f
typedef pair<int,int> pii;
vector<pii>vec[MAX];
int dis[MAX];
void dij(int s)
{
memset(dis,INF,sizeof(dis));
dis[s]=0;
int qu[MAX],he=0,ta=0,i;
qu[ta++]=s;
while(he<ta)
{
int u=qu[he++];
for(i=0;i<vec[u].size();i++)
{
int v=vec[u][i].first;
int w=vec[u][i].second;
if(dis[v]>dis[u]+w)
{
dis[v]=dis[u]+w;
qu[ta++]=v;
}
}
}
}
int main()
{
int n,m,i;
while(scanf("%d%d",&n,&m)!=EOF)
{
for(i=1;i<=m;i++)
{
int u,v,w;
scanf("%d%d%d",&u,&v,&w);
vec[u].push_back(pii(v,w));
}
int s;
cin>>s;
dij(s);
for(i=1;i<=n;i++)
cout<<dis[i]<<" ";
}
return 0;
}
堆优化的dijkstra算法
如果可以的话,就直接写这个
上面算法时间复杂度为O(N2)
而优化后为O(NlogN)
对于大数据的非负权图来说,数据量到1e5时,不优化肯定会TLE
//堆优化的dijkstra算法
typedef long long ll;
const ll inf = 2147483647;
const ll maxn = 1e5+9;
ll n, m;
ll dis[maxn]; //距离数列
ll vis[maxn]; //访问数列
struct node{
ll id, d; //节点序号、距离
bool operator < (const node &a)const{ //一定要加,定义为小顶堆
return d > a.d;
}
node(){}
node(ll _id, ll _d){
id = _id;
d = _d;
}
};
vector<node> v[maxn];
void init(){
memset(vis, 0, sizeof(vis));
for(ll i = 0; i <= n; ++i){
dis[i] = inf;
}
}
//类似bfs实现
void dijkstra(ll s){ //start节点
init(); //初始化数据
dis[s] = 0; //自己到自己的距离为0
priority_queue<node> q; //优先队列
q.push(node(s, 0)); //
while(!q.empty()){
node now = q.top(); //下一节点
q.pop();
if(!vis[now.id]){ //如果没有访问
vis[now.id] = 1; //修改访问状态
for(ll i = 0; i < v[now.id].size(); ++i){ //遍历邻接边
ll nex = v[now.id][i].id; //下一个节点序号
ll cost = v[now.id][i].d; //下一个节点费用
if(cost + now.d < dis[nex]){ //是否需要更新
dis[nex] = cost + now.d; //更新
q.push(node(nex, dis[nex])); //加入新节点
}
}
}
}
}
void test01()
{
ll s, from, to, w; //起点
cin >> n >> m >> s;
for(ll i = 0; i < m; ++i){
cin >> from >> to >> w;
v[from].push_back(node(to, w));
}
dijkstra(s);
for(ll i = 1; i <= n; ++i){
cout << dis[i] << " ";
}
}
还是每次找基点,更新邻点。
为什么这种就可以是eloge,而上面那种是n^2呢。
原因在于下面这种用堆来优化了每次找离起点最近的点的时间复杂度。
并且用邻接表优化了对于每一个基点,更新它的所有邻边的时间复杂度(上面那个就是1扫到n,很花时间)
使用stl自带的堆priority_queue,大大降低了编程复杂度。
不过如果是手写二叉堆的话,那么时间复杂度会好一个常数,是elogn。
原因在于stl的堆不能更改堆中的节点,而二叉堆可以直接改堆中的节点,即松弛操作。
不过编程复杂度太高了
Bellman-ford 算法
还没学,但是有SPFA了
形式上像BFS
关于Dijkstra、Bellman-ford以及SPFA的区别以及应用场景
dijkstra必须要搞明白,而且需要知道dijkstra如何优化 bellman-ford可以不用知道,直接看spfa
bellman-ford和spfa都可以用于解决负边权图
SPFA
在非负边权图时不要用,一般都有问题,会被卡数据
SPFA为队列优化的Bellman-ford算法
//SPFA算法实现
//核心部分比较迷
#include <iostream>
#include <ctime>
#include <vector>
#include <cstdlib>
#include <sstream>
#include <algorithm>
#include <cstring>
#include <queue>
using namespace std;
const int N = 1e3 + 9;
const int M = 1e4 + 9;
const int inf = 0x3f3f3f3f;
struct edge{
int v, w, fail;
edge(){}
edge(int _v, int _w, int _fail){
v = _v;
w = _w;
fail = _fail;
}
}e[M*2];
int head[N], len;
void init(){
memset(head, -1, sizeof(head));
len = 0;
}
void insert(int u, int v, int w){
e[len] = edge(v, w, head[u]);
head[u] = len++;
}
void insert2(int u, int v, int w){
insert(u, v, w);
insert(v, u, w);
}
int n, m;
int dis[N];
int vis[N];
void spfa(int u;){
memset(vis, false, sizeof(vis));
vis[u] = true;
memset(dis, 0x3f, sizeof(dis));
dis[u] = 0;
queue<int> q;
q.push(u);
while(!q.empty()){
u = q.front();
q.pop();
vis[u] = false;//为什么是false
//核心,但是不太明白head数组的含义
//head数组的含义是邻接表的链表头部,即节点定义上相连的第一条边
for(int j = head[u]; ~j; j = e[j].fail){
int v = e[j].v;
int u = e[j].u;
if(dis[v] > dis[u] + w){
dis[v] = dis[u] + w;
if(!vis[v]){ //没有访问就插入
q.push(v);
vis[v] = true;
}
}
}
}
}
void test01()
{
init();
int u, v, w;
cin >> n >> m;
while(m--){
cin >> u >> v >> w;
insert2(u, v, w);
}
spfa(1);
cout <<
}
int main()
{
double start = clock();
test01();
double end = clock();
cout <<"程序用时" << end - start << "ms" << endl;
return 0;
}
Floyd弗洛伊德算法
Floyd算法没什么好说的,三层循环搞清楚就可以了,直接背模板
弗洛伊德算法基本思想就是:
(1) 最开始只允许经过1号顶点进行中转;
(2) 接下来只允许经过1和2号顶点进行中转,以此类推;
(3)直到最后允许经过1~n号所有顶点进行中转,求任意两点之间的最短路程。
算法基本思路:在只经过前k号点的前提下,更新从i号顶点到j号顶点的最短路程。
//Floyd多源最短路径算法
#include <iostream>
#include <ctime>
#include <vector>
#include <cstdlib>
#include <sstream>
#include <algorithm>
#include <cstring>
//动态规划思想
//状态转移方程
//不经过第k个点,就是dp[k-1][i][j]
//经过第k个点,就是dp[k-1][i][k]+dp[k-1][k][j]
//含义:dp[k][i][j]:i到j能经过1~k的点的最短路,dp[0][i][j]就是原图
//1~k可由1~k-1状态得到
//三重循环k,i,j
//注意枚举顺序不要错
using namespace std;
const int N = 1e3 + 9;
//3 3
//1 2 5
//2 3 5
//3 1 2
//邻接矩阵
int g[N][N];
//该写法优化了一维空间
//这里dp数组退化为了g数组
void Floyd(int n){
for(int k = 1; k <= n; ++k){
for(int i = 1; i <= n; ++i){
for(int j = 1; j <= n; ++j){
g[i][j] = min(g[i][j], g[i][k] + g[k][j]);
}
}
}
}
void test01()
{
memset(g, 0x3f, sizeof(g));
for(int i = 0; i < N; ++i){
g[i][i] = 0; //自环为0
}
//输入,创建邻接矩阵
int n, m;
int u, v, w;
cin >> n >> m;
while(m--){
cin >> u >> v >> w;
g[u][v] = g[v][u] = w;
}
Floyd(n);
//打印
for(int i = 1; i <= n; ++i){
for(int j = 1; j <= n; ++j){
cout << g[i][j] << " ";
}
cout << endl;
}
}
int main()
{
double start = clock();
test01();
double end = clock();
cout <<"程序用时" << end - start << "ms" << endl;
return 0;
}
K短路问题
目前没考过,我也不会
好,现在会了,采用A*算法
代码思路
//1. 一般我们会采用BFS来求解最短路问题
//2. 如果我们采用单纯采用BFS,那就是第k次到达终点的长度是k短路
//3. 因此这里我们采用A*算法,启发函数采用f=x + h(x为到当前点的实际距离,h为当前点到终点的估测最近距离)
//那么我们怎么求这个h呢,平常我们采用dijkstra单源最短路径求最短路
//这里我们当然就是采用反向建图来求出终点到各个点的最小路径啦
//因此这道题我们需要采用正向+反向建图
//正向建图求解BFS
//反向建图求解最短路
//poj2449为例
以poj2449为例
//k短路
#include <iostream>
#include <ctime>
#include <vector>
#include <cstdlib>
#include <sstream>
#include <algorithm>
#include <cstring>
#include <queue>
using namespace std;
typedef long long ll;
const ll maxn = 1e5+9;
//5 8
//1 2 5
//1 3 10
//2 3 1
//2 5 9
//3 4 2
//1 5 5
//4 5 4
//2 4 7
//1 5 3
struct node{
ll to, w;
node(){}
node(ll _to, ll _w){
to = _to;
w = _w;
}
operator < (const node& a) const {
return w < a.w;
}
};
ll n, m, st, ed, k;
vector<node> zv[maxn]; //正向建图
vector<node> fv[maxn]; //反向建图
bool vis[maxn];
ll dis[maxn];
//反向建图,求解终点到各点的最短路
void dijkstra(){
priority_queue<node> q;
q.push(node(ed, 0));
while(!q.empty()){
node now = q.top();
q.pop();
ll cur_id = now.to;
if(!vis[cur_id]){
vis[cur_id] = 1;
for(ll i = 0; i < fv[cur_id].size(); ++i){
ll tid = fv[cur_id][i].to;
ll tw = fv[cur_id][i].w;
if(dis[tid] > dis[cur_id] + tw){
dis[tid] = dis[cur_id] + tw;
q.push(node(tid, dis[tid]));
}
}
}
}
}
ll astar(ll s){
//类似BFS过程
//实际上当h=0时,A*算法就会退化为BFS算法
priority_queue<node> q;
q.push(node(s, 0));
while(!q.empty()){
node now = q.top();
q.pop();
ll cur_id = now.to;
ll cur_w = now.w;
if(cur_id == ed){ //到达终点
k--;
if(k == 0) return cur_w;
}
for(int i = 0; i < zv[cur_id].size(); ++i){
ll tid = zv[cur_id][i].to;
ll tw = zv[cur_id][i].w;
q.push(node(tid, cur_w + tw));
}
}
}
void test01()
{
cin >> n >> m; //n point m edge
memset(vis, 0, sizeof(vis));
memset(dis, 0x3f3f3f3f, sizeof(dis));
ll from, to, w;
for(ll i = 0; i < m; ++i){
cin >> from >> to >> w;
zv[from].push_back(node(to, w));
fv[to].push_back(node(from, w));
}
cin >> st >> ed >> k; //起点。终点,k短
dijkstra();
if(dis[st] == 0x3f3f3f3f) cout << -1 << endl; //不可到达
if(st == ed) k++; //起点和终点重合,排除0距离
cout << astar(st);
}
9. 最小生成树:
概述
生成树算法
连通图中的生成树必须满足以下 2 个条件:
- 包含连通图中所有的顶点;
- 任意两顶点之间有且仅有一条通路;
因此,连通图的生成树具有这样的特征,即生成树中边的数量 = 顶点数 - 1
这就是本节要讨论的最小生成树的问题,简单得理解就是给定一个带有权值的连通图(连通网),如何从众多的生成树中筛选出权值总和最小的生成树,即为该图的最小生成树。
给定一个连通网,求最小生成树的方法有:
1.普里姆(Prim)算法
2.克鲁斯卡尔(Kruskal)算法。
Prim算法概述
设G=(V,E)是无向连通带权图,即一个网络。E中每条边(v,w)的权为c[v][w]。
如果G的子图G’是一棵包含G的所有顶点的树,则称G’为G的生成树。生成树上各边权的总和称为该生成树的耗费。
在G的所有生成树中,耗费最小的生成树称为G的最小生成树。
网络的最小生成树在实际中有广泛应用。
例如,在设计通信网络时,用图的顶点表示城市,用边(v,w)的权c[v][w]表示建立城市v和城市w之间的通信线路所需的费用,则最小生成树就给出了建立通信网络的最经济的方案。
最小生成树性质
设G=(V,E)是连通带权图,U是V的真子集。如果(u,v)属于E,且u属于U,v属于V-U,且在所有这样的边中,(u,v)的权c[u][v]最小,
那么一定存在G的一棵最小生成树,它以(u,v)为其中一条边。这个性质有时也称为MST性质。
哈夫曼树
哈夫曼构造过程
- 每次取出权值最小的两个结点,两权值相加为新节点,且为刚才两节点的父节点
- 同时该新节点放回序列,开始找新的最小的两个权值,不断重复。
#include<bits/stdc++.h>
using namespace std;
priority_queue<int, vector<int>, greater<int> > Q;//建立一个小顶堆
int main(){
int n;
while(scanf("%d", &n)!=EOF){
while(Q.empty()==false) Q.pop();
for(int i=1; i<=n; i++){//输入n个叶子结点权值
int x;
scanf("%d", &x);
Q.push(x);//将权值放入堆中
}
int ans = 0;//保存答案
while(Q.size()>1){//当堆中元素大于1个
int a = Q.top();
Q.pop();
int b = Q.top();
Q.pop();//取出堆中两个最小元素,他们为同一个结点的左右儿子,且该双亲结点的权值为它们的和
ans += a + b;//该父亲结点必为非叶子结点,故累加其权值
Q.push(a+b);//将该双亲结点的权值放回堆中
}
cout << ans << endl; //输出答案
}
return 0;
}
PRIM算法
https://www.cnblogs.com/kannyi/p/8587733.html
设G=(V,E)是连通带权图,V={1,2,…,n}。
构造G的最小生成树的Prim算法的基本思想是:首先置S={1},然后,只要S是V的真子集,就作如下的贪心选择:选取满足条件i属于S,j属于V-S,且c[i][j]最小的边,将顶点j添加到S中。这个过程一直进行到S=V时为止。
在这个过程中选取到的所有边恰好构成G的一棵最小生成树。
用这个办法实现的Prim算法所需的计算时间为O(n2)。
模板
n是顶点数,m是边数;
edge[i][j]表示边e=(i,j)的权值,不存在的情况下设为INF,所以要记得提前做赋值操作;
mincost[i]数组表示已访问点的集合到每个未访问点 i 的最小权值;
vis[i]表示顶点 i 是否已访问
#include<bits/stdc++.h>
#define MAX 1005
#define INF 0x3f3f3f3f
using namespace std;
int n,m,edge[MAX][MAX],mincost[MAX],vis[MAX];
void init()
{
int u,v,w,i;
memset(edge,INF,sizeof(edge));
for(i=1;i<=m;i++)
{
scanf("%d%d%d",&u,&v,&w);
edge[u][v]=edge[v][u]=w;
}
for(i=1;i<=n;i++)
{
vis[i]=0;
mincost[i]=INF;
}
mincost[1]=0;
}
int prim()
{
int i,v,cost=0;
init();
while(1)
{
v=-1;
for(i=1;i<=n;i++)//遍历所有节点
if(!vis[i]&&(v==-1||mincost[i]<mincost[v]))//i节点没有被访问过,找出没被访问节点中的最短距离
v=i;
if(v==-1)
break;//已经完成
vis[v]=1;//修改该点已访问过
cost+=mincost[v];//加入最小生成树
for(i=1;i<=n;i++)
mincost[i]=min(mincost[i],edge[i][v]);//更新所有节点到新加入节点的最小距离
}
return cost;
}
int main()
{
cin>>n>>m;
cout<<prim()<<endl;
return 0;
}
并查集+克鲁斯卡尔
在学习之前首先要学习并查集的相关知识
知道并查集如何实现已经并查集的用途
并查集是一种简单的用途广泛的集合。
并查集是若干个不相交集合,能够实现较快的合并和判断元素所在集合的操作。
并查集可以进行路径压缩
我们不考虑具体树的实现,我们只关心节点的根节点
只要有同一个根节点,那么这些节点都在一个集合内
#define maxn 110
#define maxm 10010
using namespace std;
int uf[maxn];
struct edge{
int u,v,len;
}e[maxm];
bool cmp(const edge &x,const edge &y){
return x.len<y.len;
}
//下面为并查集的数据结构
void init(int n){//初始化并查集
for(int i=0;i<=n;i++)
uf[i]=i;
}
//路径压缩
int find(int x){
if(x==uf[x])return x;
return uf[x]=find(uf[x]);
}
int Union(int x,int y){//合并两个集合(如果x,y在同一集合,返回0,否则返回1)
x=find(x);
y=find(y);
if(x!=y){
uf[x]=y;
return 1;
}
return 0;
}
//
int Kruskal(int n,int m){//n个点,m条边
sort(e,e+m,cmp);//排序
int sum=0;//最小生成树的权值和
for(int i=0;i<m;i++){//从小到大枚举边
int u=e[i].u,v=e[i].v,len=e[i].len;
sum+=len*Union(u,v);
}
return sum;//返回权值和
}
小总结
一般来说,kruskal用的更多
Kruskal和Prim的区别
Kruskal在所有边中不断寻找最小的边
Prim在U和V两个集合之间寻找权值最小的连接。
共同点
构造过程中都不能形成环!
时间上
Prim适合稠密图,复杂度为O(nn),因此通常使用邻接矩阵储存;
Kruskal适合稀疏图,多用邻接表储存,复杂度为O(eloge)。
空间上
Prim适合边较多的情况;
Kruskal适合点较多的情况。
10. 二分法
二分查找
const int N = 1e3+9;
int a[N], n, m;
int find(int x){
int l = 0; //左指针
int r = n-1; //右指针
while(l<=r){
int mid = (l+r)>>1;
if(a[mid] == x){
return mid;
}
if(a[mid] < x){
l = mid + 1; //右边一个
}else{
r = mid - 1; //左边一个
}
}
return -1;
}
void test01()
{
cin >> n;
for(int i = 0; i < n; ++i){
cin >> a[i];
}
cin >> m;
while(m--){
int x;
cin >> x;
cout << find(x) << endl;
}
}
二分答案
二分答案也是一个重点
二分答案的前提是答案的单调性
作为二分查找的应用有如下4种题型
1 最大的最小值
2 最小的最大值
//将n个数进行分组,分k组,求分组总和最大值的最小值
//分组数据之间是连续的
//1 2 3 4 5 6 7 8 9 10
//0 0 0 0 1 1 1 1 1 1
//判断分组总和最大值是否合法
int n, k;
int a[N];
bool ok[N];
//8 4
//1 3 4 7 1 4 3 8
//如果出现多个分组,说明x不是最大值,还可以更大
//如果刚好或者不足k个分组,说明
bool check(int x){ //判断mid为最大值进行分组是否可以,使用贪心算法,从第一个组开始,尽可能往里面放
int now = 0, cnt = 0;
for(int i = 0; i < n; ++i){
if(now + a[i] > x){
cnt++;
now = a[i];
}else{
now += a[i];
}
}
return cnt <= k;
}
int cal(int l, int r){
while(l < r){
int mid = (l+r) >> 1;
if(check(mid)){
r = mid;
}else{
l = mid+1;
}
}
return l;
}
void test01()
{
cin >> n >> k;
int min_num = 0;
int max_num = 0;
for(int i = 0; i < n; ++i){
cin >> a[i];
min_num = max(min_num, a[i]);
max_num += a[i];
}
cout << cal(min_num, max_num) << endl;
}
3 满足条件下的最小/大值
题意
将n个馅饼分给f个朋友,使得每人拿到的一样大,且每个人只能拿一整块,求每个人能拿到的最大值
分析
答案:每个人拿到的馅饼的最大值,条件:每人拿到的一样大,且每个人只能拿一整块;可以选择二分半径的区间,通过半径计算答案,根据条件每人只能拿一整块,那么每块馅饼最多能分成(Vi/mid)个,再来判断能否分成 f+1个(自己也要吃一个)
#include <cmath>
#include <cstdio>
#include <iostream>
using namespace std;
const double eps = 1e-10;
const double pi = 4*atan(1.0);
int n,f,arr[10100];
bool check(double r)
{
double s = r*r;
int num = 0;
for(int i=1; i<=n; ++i)
{
double tep = arr[i]*arr[i];
num += (int)(tep/s);
}
return num >= f+1? true : false;
}
int main()
{
int t;
cin>>t;
while(t--)
{
cin>>n>>f;
for(int i=1; i<=n; ++i)
cin>>arr[i];
double l = 0.0;
double r = 10100.0;
while(fabs(r-l)>eps) //注意精度
{
double mid = (l+r)/2.0;
if(check(mid))
l = mid;
else r = mid;
}
printf("%.4f\n",r*r*pi);
}
return 0;
}
lower_bound函数与upper_bound函数
(1) 在使用这两个函数前,必须保证该数组是非递减序列。
(2) lower_bound(begin,end,num):
从数组的begin位置到end-1位置二分查找第一个≥num的元素,找到则返回该元素的地址,不存在则返回end。
(3) upper_bound(begin,end,num):
从数组的begin位置到end-1位置二分查找第一个>num的元素,找到则返回该元素的地址,不存在则返回end。
(4) 通过返回的地址减去起始地址begin,我们就可以得到该元素在数组中的下标。
11. 线段树与树状数组:
差分数组
BLOG
要使差分数组[ 1, 5 ] 之间的每个元素都+1,用差分数组只需要这样写:
d[ 1 ]++;
d[ 5+1 ]--;
Holidays
//差分数组-Holidays
const int maxn = 105;
int n, m;
int from, to;
int c[maxn];
void test01()
{
cin >> n >> m;
for(int i = 1; i <= m; ++i){
cin >> from >> to;
//from-to区间所有都+1
c[from]++;
c[to+1]--;
}
for(int i = 1; i <= n; ++i){
c[i] = c[i-1] + c[i]; //还原数组
if(c[i] == 0){ //没浇水
cout << i << " " << 0 << endl;
return ;
}
if(c[i] > 1){
cout << i << " " << c[i] << endl;
return ;
}
}
cout << "OK" << endl;
}
树状数组维护
const int maxn = 105;
int n, m;
int from, to;
int c[maxn];
int lowbit(int i){
return i&(-i);
}
void update(int x, int v){
for(int i = x; i <= n; i += lowbit(i)){
c[i] += v;
}
}
int getsum(int x){
int ans = 0;
for(int i = x; i > 0; i -= lowbit(i)){
ans += c[i];
}
return ans;
}
void test01()
{
cin >> n >> m;
for(int i = 1; i <= m; ++i){
cin >> from >> to;
update(from, 1); //差分数组修改{c[from]++}
update(to+1, -1); //差分数组修改{c[to+1]--;}
}
for(int i = 1; i <= n; ++i){
int s = getsum(i);
if(s != 1){
cout << i << " " << s << endl;
return ;
}
}
cout << "OK" << endl;
}
海底高铁
//差分与前缀和-海底高铁
typedef long long ll;
const ll maxn = 1e5+9;
ll n, m;
//ll a1, b1, c1;
ll a[maxn]; //目的地
ll b[maxn][3]; //A,B,C
ll c[maxn];
//A:纸质票;B:IC卡扣费;C:IC卡工本费
ll read(){
int s = 0, w = 1;
char ch = getchar();
while(ch < '0' || ch > '9'){
ch = getchar();
}
while(ch >= '0' && ch <= '9'){
s = (s << 3) + (s << 1) + ch - '0';
ch = getchar();
}
return s;
}
ll lowbit(ll i){
return i&(-i);
}
void update(ll x, ll v){
for(ll i = x; i <= n ; i += lowbit(i)){
c[i] += v;
}
}
ll getsum(int x){
ll ans = 0;
for(ll i = x; i > 0; i -= lowbit(i)){
ans += c[i];
}
return ans;
}
void test01()
{
n = read(); m = read();
for(ll i = 1; i <= m; ++i){
a[i] = read();
}
for(ll i = 2; i <= m; ++i){
//差分数组
ll sm = a[i];
ll bg = a[i-1];
if(a[i-1] < a[i]){
sm = a[i-1];
bg = a[i];
}
update(sm, 1);
update(bg, -1);
}
ll ans = 0;
for(ll i = 1; i <= n-1; ++i){
//比较选择选择纸质票还是IC卡
b[i][0] = read(); b[i][1] = read(); b[i][2] = read();
ll s = getsum(i); //经过的次数
ll zhi = s*b[i][0];
ll ic = s*b[i][1] + b[i][2];
if(zhi > ic){
ans += ic;
}else{
ans += zhi;
}
}
cout << ans << endl;
}
前缀和(当然前缀合最好使用树状数组可离线查询)
c[i] = c[i-1] + c[i]; //还原数组
https://www.cnblogs.com/ailanxier/p/13419109.html
线段树与树状数组:
有一类区间问题可以抽象成如下模型:
给定包含n个数的数组a1,a2…an,有两种操作
1 查询区间[l,r]最小的数;
2 修改第ai为x
线段树时间复杂度:O(logn)
图和树:树:无向、连通图,无环
对于一棵有n个节点的树,有且只有n-1个节点
特殊的树:
二叉树:
一个节点最多两个子节点
叶子节点、根节点
完全二叉树:
对于一个满二叉树,有2^n-1个节点
完全二叉树是少尾元素的man二叉树
线段树能解决超多有关区间的问题。
单点修改,单点查询,区间修改,区间查询
应用范围比树状数组广,变通性极强(树状数组能解决的问题线段树都能解决,但是后者能解决的一些问题树状数组还是搞不了的,但是树状数组时空常数小,代码量少,还不容易写错)。
线段树可以区间维护区间和、区间乘,区间根号,区间最大公因数,连续的串长度等、区间最值操作等。
https://www.luogu.com.cn/training/206#problems
https://ac.nowcoder.com/acm/problem/collection/621
#include<bits/stdc++.h>
using namespace std;
#define For(i,sta,en) for(int i = sta;i <= en;i++)
#define speedUp_cin_cout ios::sync_with_stdio(false);cin.tie(0); cout.tie(0);
#define ls now<<1
#define rs now<<1|1
#define mid (l+r)/2
typedef long long ll;
const int maxn = 1e5+5;
int n,m;
struct node{
ll sum,lazy; //sum为区间和,lazy为懒标记
}t[maxn<<2];
void pushup(int now){
t[now].sum = t[ls].sum + t[rs].sum;
}
void build(int now,int l,int r){
if(l == r) { cin>> t[now].sum ; return;}
build(ls,l,mid);
build(rs,mid+1,r);
pushup(now);
}
void pushdown(int now,int tot){
t[ls].lazy += t[now].lazy; //懒标记给左孩子
t[rs].lazy += t[now].lazy; //懒标记给右孩子
t[ls].sum += (tot - tot/2) * t[now].lazy; //区间和加上懒标记的影响,注意范围
t[rs].sum += (tot/2) * t[now].lazy;
t[now].lazy = 0; //记得懒标记下传后清0
}
void update(int now, int l, int r, int x, int y, int value){
if(x <= l && r <= y) {t[now].lazy += value; t[now].sum += (r - l + 1) * value;return;}
if(t[now].lazy) pushdown(now,r-l+1); //懒标记下传,与本次修改的信息无关,只是清算之前修改积压的懒标记
if(mid >= x) update(ls, l, mid, x, y, value);
if(mid < y) update(rs, mid + 1, r, x, y, value);
pushup(now);
}
ll query(int now, int l, int r, int x, int y){
if(x <= l && r <= y) return t[now].sum;
if(t[now].lazy) pushdown(now,r-l+1);
ll ans = 0;
if(mid >= x) ans += query(ls, l, mid, x, y);
if(mid < y) ans += query(rs, mid + 1, r, x, y);
return ans;
}
int main(){
speedUp_cin_cout//加速读写
cin>>n>>m;
build(1,1,n); //建树顺便读入,省一个数组
int op,l,r,d;
For(i,1,m){
cin>>op;
if(op == 1) { // l 到 r 加上 d
cin>>l>>r>>d;
update(1, 1, n, l, r, d);
}else {
cin>>l>>r; //查询 l 到 r 的值
cout << query(1, 1, n, l, r) << endl;
}
}
return 0;
}
线段树:
我们用一棵二叉树表示线段树,每个节点表示一个区间
根节点编号1,每个节点,左节点为2i,右节点为2i+1
对于一个节点,如果其表示的区间为[l,r],若l=r,为叶子节点
否则mid = [(l+r)/2],左区间为[l,mid],右区间为[mid+1,r]
有点像二分
我们用一个额外的minv数组存储每个节点对应区间的最小值
min ai = min(minv(l,mid), minv(mid+1,r));
左右孩子中较小的将需要处理的值放入叶子节点,再进行min操作
父节点需要子节点进行更新,我们用递归方式进行更新
const int maxn = 10010;
int minv[4*maxn], d[maxn]
//id表示节点编号,l,r为区间
void biuld(int id, int l, int r){
if(l == r){
minv[id] = a[l]; //叶子
return ;
}
int mid = (l+r)/2;
build(id>>1, l, mid);//左边为id*2
build(id>>1 + 1, mid+1, r);//右边为id*2+1
minv[id] = min(minv[id<<1], minv(id<<1 + 1));
}
注意要用4*maxn创建数组,否则空间不够
创建树的时间复杂度为O(N)
线段树的单点更新:
整个数据结构不需要推倒重来
整个包含该点的区间在树上实际是一条链时间复杂度依然为树高O(logn)
//x位置换成v
void update(int id, int l, int r, int x, int v){
if(l == r){
minv[id] = v;
return ;
}
int mid = (l+r)/2;
//需要更改的在左,只要修改左,否则为右,确保只有一条链
if(x <= mid){
update(id<<1, l, mid, x, v);
}else{
update(id<<1 | 1, mid+1, r, x, v);
}
//实际更新操作
minv[id] = min(minv[id<<1], minv[id<<1 | 1]);
}
单点查询操作:
和更新比较像,一直沿着链走到叶子即可
int query(int id, int l, int r, int x){
if(l == r){
return minv[id];
}
int mid = (l+r)/2;
if(mid <= x){
return query(id << 1, l, mid, x);
}else{
return query(id << 1 | 1, mid+1, r, x);
}
}
区间查询:
单点查询是区间查询的特殊情况
大区间可由小区间并起来
每一层最多有两个中间节点,最左边一个和最右边一个
红点表示尾结点,绿色为中间节点,最多有2logn个绿色节点,时间复杂度也是logn
int query(int id, itn l, int r, int x, int y){
if(x <= l && r <= y){ //如果完全
return midv[id];
}
int mid = (l+r)/2;
int ans = inf;
if(x <= mid){
ans = min(ans, query(id<<1, l, mid, x, y));
}
if(y > mid){
ans = min(ans, query(id<<1 | 1, mid+1, r, x, y));
}
return ans;
}
树状数组+扫描线
暂时没更新
树状数组
线段树代码比较长,但是可以解决树状数组可以解决的所有问题
树状数组代码比较短,对于部分问题有可以极好解决
知识基础
公式lowbit(x) = x & (-x)
补码性质不需要理解
010101000&101011000-》000001000
lowbit可以得到数据x的二进制的最后一个1的数值区别
我们知道线段树是按照二分中间划分的,而树状数组是根据lowbit划分的
线段树的节点区间递归得到,
而树状数组的一个节点表示的区间可以根据节点标号计算出
对于节点i,区间为**[i-lowbit(i)+1, i]**()
我们再用一个数组C表示每个节点对应的区间的数的和
Ci=a(i-lowbit(i)+1)+a(i-lowbit(i)+2)…+ai
树状数组查询:
要查询[l,r]的和,可以先求出[l,r]的和值,然后减去[1,l-1]的和值
现在关键是如何求[1,x]上的和值
步骤:
令sum=0
加上区间[x-lowbit(x)+1, x]的和值
然后x = x - lowbit(x)
当x=0时退出,否则循环
查询效率为logx
int getsum(int x){
int res = 0;
for(int i = x; i > 0; i -= lowbit(i)){
res += C[i];
}
return res;
}
更新:
如果让x增加v,那么只有包含位置x的区间和值才会受到影响。
计算受影响区间技巧:x = x + lowbit(x),直到x>n,路上的x都是受影响的
void update(int x, int v){
for(int i = v; i <= n; i += lowbit(i)){
C[i] += v;
}
}
完整代码
#include<bits/stdc++.h>
using namespace std;
#define For(i,sta,en) for(int i = sta;i <= en;i++)
#define speedUp_cin_cout ios::sync_with_stdio(false);cin.tie(0); cout.tie(0);
#define lowbit(x) x&(-x)
typedef long long ll;
const int maxn = 5e5+5;
int t[maxn],n,m,num;
void update(int now,int value){
while(now<=n){
t[now]+=value;
now += lowbit(now);
}
}
ll query(int now){
ll ans = 0; //long long 类型的答案
while(now){
ans += t[now];
now -= lowbit(now);
}return ans;
}
int main(){
speedUp_cin_cout
cin>>n>>m;
For(i,1,n) {
cin>>num;
update(i,num);
}int op,x,y;
For(i,1,m){
cin>>op>>x>>y;
if(op == 1) update(x,y);
else cout<<query(y)-query(x-1)<<endl;
}
return 0;
}
K小/大值问题
#include <iostream>
#include <vector>
#define lowbit(x) ((x) & (-x))
using namespace std;
const int N = 1025;
int c[N] = {0};
void add(int x, int v) {
for (int i = x; i < N; i += lowbit(i)) c[i] += v;
}
int getSum(int x) {
if (x < 1) return 0;
else return c[x] + getSum(x - lowbit(x));
}
int getKthLeast(int k) {
int left = 1, right = N - 1, mid;
while (left < right) {
mid = (left + right) / 2;
if (getSum(mid) >= k) right = mid;
else left = mid + 1;
}
return left;
}
int main() {
int n, k, temp, cnt = 0;
while (true) {
while (cin >> temp, temp != -1) {
add(temp, 1); //离散化处理
}
printf("Input the k to search: ");
scanf("%d", &k);
if (k != -1) {
printf("The k-th least number is %d\n", getKthLeast(k));
break;
} else continue;
}
return 0;
}
逆序对问题
逆序对就是如果i > j && a[i] < a[j],这两个就算一对逆序对。
其实也就是对于每个数而言,找找排在其前面有多少个比自己大的数。
※一旦出现最小交换次数,一定要先考虑逆序对问题
int n;
int a[1005],c[1005]; //对应原数组和树状数组
int lowbit(int x){
return x&(-x);
}
void updata(int i,int k){ //在i位置加上k
while(i <= n){
c[i] += k;
i += lowbit(i);
}
}
int getsum(int i){ //求A[1 - i]的和
int res = 0;
while(i > 0){
res += c[i];
i -= lowbit(i);
}
return res;
}
思路
树状数组又一次地优化了这种“需要遍历”的情况。那不就很容易了吗?依次把序列里的数放到树状数组中的A[i]上去(实际是以C[i]形式的插入函数),注意A[i]是以数值大小从小到大排列的。先插入的说明排在序列的前面,那么后插入的就可以看看之前插入的比你大的数有多少,即i-sum(i),其实也就是看序列前面比你大的数有多少个,即找逆序对。
//求逆序对的核心
//在处理到第i个数的时候,i−query(a[i]) 表示有前i个数之中有几个数比第i个数大
//我觉得很神奇,直接得到“离散化”的结论:把原序列中每个元素的值和下标存到一个结构体node里去,之后把node数组按元素值大小从小到大排序(注意结构体//里的重载<运算符的写法 不是男左女右了我结论有误T T 反正考场上试一试即可),这样得到的结点的下标值即是离散化结果,等效于原序列的数值。把这些下//标值当成原序列,按照树状数组求逆序对的原理做。
#include<iostream>
#include<bits/stdc++.h>
using namespace std;
const int maxn=500001;
int c[maxn]; //构建树状数组
struct Node
{
int v,index;
bool operator < (const Node &b) const
{
return v<b.v; //从小到大排序
}
}node[maxn];
int n;
void update(int v){
for(int i = v; i <= n; i += lowbit(i)){
C[i] += v;
}
}
//void add(int i)
{
while(i<=n)
{
c[i]++;
i+=i&(-i);
}
}
int getsum(int x){
int res = 0;
for(int i = x; i > 0; i -= lowbit(i)){
res += C[i];
}
return res;
}
//long long sum(int i)
//{
// long long res=0;
while(i>0)
{
res+=c[i];
i-=i&(-i);
}
return res;
}
int main()
{
cin>>n;
int a;
for(int i=1;i<=n;i++)
{
scanf("%d",&a);
node[i].index=i;
node[i].v=a;
}
sort(node+1,node+1+n); //按照值排序
long long ans=0;
for(int i=1;i<=n;i++)
{
add(node[i].index); //离散化结果—— 下标等效于数值
ans+=i-sum(node[i].index); //得到之前有多少个比你大的数(逆序对)
}
cout<<ans;
return 0;
}
线段树实现逆序对
//线段树逆序对
#define lson o<<1, l, m
#define rson o<<1|1, m+1, r
using namespace std;
typedef long long LL;
const int maxn = 500500;
const int MAX = 0x3f3f3f3f;
int n, a, b, in[maxn], tt[maxn], fu[maxn], f[maxn];
LL num[maxn<<2];
int bs(int v, int x, int y) {
while(x < y) {
int m = (x+y) >> 1;
if(fu[m] >= v) y = m;
else x = m+1;
}
return x;
}
void up(int o) {
num[o] = num[o<<1] + num[o<<1|1];
}
void build(int o, int l, int r) {
num[o] = 0;
if(l == r) return ;
int m = (l+r) >> 1;
build(lson);
build(rson);
}
void update(int o, int l, int r) {
if(l == r) {
num[o]++;
return ;
}
int m = (l+r) >> 1;
if(a <= m) update(lson);
else update(rson);
up(o);
}
LL query(int o, int l, int r) {
if(a <= l && r <= b) return num[o];
int m = (l+r) >> 1;
LL ans = 0;
if(a <= m) ans += query(lson);
if(m < b ) ans += query(rson);
return ans ;
}
int main()
{
while(cin >> n, n) {
for(int i = 0; i < n; i++) {
scanf("%d", &in[i]);
tt[i] = in[i]; //tt记录原序列
}
sort(in, in+n);
int k = 0;
fu[k++] = in[0]; //fu为辅助数组
for(int i = 1; i < n; i++)
if(in[i] != in[i-1]) fu[k++] = in[i];
b = 0;
for(int i = 0; i < n; i++) { //离散过程,二分
f[i] = bs(tt[i], 0, k-1);
b = max(b, f[i]);
}
LL ans = 0;
build(1, 0, b);
for(int i = 0; i < n; i++) {
a = f[i] + 1; // 查询f[i]+1~n的个数,个数就是f[i]当前的逆序对总数
ans += query(1, 0, b);
a = f[i]; // 将f[i]添加到数组中
update(1, 0, b);
}
cout << ans << endl;
}
return 0;
}
12. *kmp字符串匹配算法和字符串相关
KMP暂时不要了,目前没有做到相关的题目
如果考试考的了类似字符串匹配和最长公共子串的话就使用string类的find函数
STL-String
- 字符匹配,返回第一次下标
1 #include <cstring>
2 #include <cstdio>
3 #include <iostream>
4 using namespace std;
5 int main()
6 {
7 find函数返回类型 size_type
8 string s("1a2b3c4d5e6f7jkg8h9i1a2b3c4d5e6f7g8ha9i");
9 string flag;
10 string::size_type position;
11 //find 函数 返回jk 在s 中的下标位置
12 position = s.find("jk");
13 if (position != s.npos) //如果没找到,返回一个特别的标志c++中用npos表示,我这里npos取值是4294967295,
14 {
15 printf("position is : %d\n" ,position);
16 }
17 else
18 {
19 printf("Not found the flag\n");
20 }
21 }
- 查找某一给定位置后的子串的位置
1 //从字符串s 下标5开始,查找字符串b ,返回b 在s 中的下标
2 position=s.find("b",5);
3 cout<<"s.find(b,5) is : "<<position<<endl;
- 查找****s 中****flag 出现的所有位置
flag="a";
position=0;
int i=1;
while((position=s.find(flag,position))!=string::npos)
{
cout<<"position "<<i<<" : "<<position<<endl;
position++;
i++;
}
- 返回子串出现在母串中的首次出现的位置**,**和最后一次出现的位置
flag = "c";
position = s.find_first_of(flag);
printf("s.find_first_of(flag) is :%d\n",position);
position = s.find_last_of(flag);
printf("s.find_last_of(flag) is :%d\n",position);
KMP算法
#include<cstdio>
#include<iostream>
#include<algorithm>
#include<string>
#include<cstring>
#include<cmath>
using namespace std;
char a1[2000000],a2[2000000];
int kmp[2000000];
int main()
{
scanf("%s%s",a1,a2);
kmp[0]=kmp[1]=0;//前一位,两位失配了,都只可能将第一位作为新的开头
int len1=strlen(a1),len2=strlen(a2);
int k;
k=0;
for(int i=1;i<len2;i++)//自己匹配自己
{
while(k&&a2[i]!=a2[k])k=kmp[k];//找到最长的前后缀重叠长度
kmp[i+1]=a2[i]==a2[k]?++k:0;//不相等的情况,即无前缀能与后缀重叠,直接赋值位0(注意是给下一位,因为匹配的是下一位适失配的情况)
}
k=0;
for(int i=0;i<len1;i++)
{
while(k&&a1[i]!=a2[k])k=kmp[k];//如果不匹配,则将利用kmp数组往回跳
k+=a1[i]==a2[k]?1:0;//如果相等了,则匹配下一位
if(k==len2)printf("%d\n",i-len2+2);//如果已经全部匹配完毕,则输出初始位置
}
for(int i=1;i<=len2;i++)printf("%d ",kmp[i]);//输出f数组
return 0;
}
13. 数论相关
位运算
GCD(欧几里得算法)
素数数论部分
很好的博客
__gcd-最大公约数
algorithm库中的函数
int main()
{
scanf("%d %d",&n,&m);
int k=__gcd(n,m);//最大公约数
printf("%d",k);
return 0;
}
递归实现
int gcd(int a, int b)//求最大公约数
{
return b == 0 ? a : gcd(b, a % b);
}
最小公倍数
int lcm(int a, int b)
{
return a * b / gcd(a, b);
}
乘法逆元
BLOG
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-cqRdNc1t-1649491696015)(C:\Users\Administrator\Desktop\屏幕截图 2022-04-05 131155.png)]
ll pow(ll x, ll p) {
x %= mod;
ll ans = 1;
while(p>0){
if(p&1 == 1) ans *= x;
x *= x;
}
}
int main() {
ll x = pow(a, p - 2); //x为a在mod p意义下的逆元
}
拓展GCD
蓝桥杯 包子凑数
拓展欧几里得:
设方程ax+by=C,方程有解的充要条件是C是gcd(a,b)的倍数
例如:
- 如果gcd(a,b)==1,即两数互质凑不出的C的最大值,最大值为:a*b-a-b(公式)
- 如果gcd(a,b)!=1,比如等于k,那么我们只能凑出k的整数倍
所以肯定有INF个数字不是K的倍数
同样设方程a1x1+a2x2+…+anxn=C(第i种蒸笼恰好能放ai个包子,第i种蒸笼拿xi笼,C为要凑出的包子数) ,C是gcd(a1,a2,…,an)的倍数,也满足上面的结论
组合数相关的
代码思路
- GCD
- EXGCD
cout << x*c/gcd(a, b) << " " << y*c/gcd(a, b) << endl;
//拓展欧几里得
#include <iostream>
#include <ctime>
#include <vector>
#include <cstdlib>
#include <sstream>
#include <algorithm>
//扩展欧几里德算法其实就是为了求出 ax+by=gcd(a,b) 的一个整数解 (x0,y0)
//然后就能得出全部整数解为 (x0+k*b,y0-k*a)
using namespace std;
//递归扩展欧几里得算法
//方程ax+by=C
//int exgcd(int a,int b,int&x,int&y)
//{
// if(b==0){
// x=1,y=0;
// return a;
// }
// int r=exgcd(b,a%b,y,x);
// y-=a/b*x;
// return r;
//}
int exgcd(int a, int b, int &x, int &y){
if(b == 0){
x = 1;
y = 0;
return a;
}
//x,y互换位置
int gcd = exgcd(b, a%b, y, x);
int tx = x;
y = y - (a/b)*x;
return gcd;
}
int gcd(int a, int b){
if(b == 0){
return a;
}
return gcd(b, a%b);
}
void test01(){
int x, y;
int a, b, c;
cin >> a >> b >> c;
exgcd(a, b, x, y);
cout << x*c/gcd(a, b) << " " << y*c/gcd(a, b);
}
快速幂
快速幂模版(迭代,非递归)
int fastpow(int a, int k) {
int res = 1;
while (k) {
if (k & 1) {
res = res * a % p;
}
a = a * a % p;
k >>= 1;
}
return res;
}
矩阵快速幂
#include <iostream>
using namespace std;
const int N = 100, mod = 1e6;
int n, k;
struct mat {
int m[N][N];
mat() { // 用构造函数初始化成 单位矩阵E
memset(m, 0, sizeof m);
for (int i = 0; i < N; i++)
m[i][i] = 1;
}
};
// 矩阵乘法
mat multi(mat a, mat b) {
mat c;
for (int i = 0; i < n; i++) {
for (int j = 0; j < n; j++) {
c.m[i][j] = 0;
for (int k = 0; k < n; k++) {
c.m[i][j] += a.m[i][k] * b.m[k][j]; // 累加
}
c.m[i][j] %= mod;
}
}
return c;
}
// 矩阵快速幂(核心)
mat fastpow(mat a, int k) {
mat res;
while (k) {
if (k & 1) res = multi(res, a);
a = multi(a, a);
k >>= 1;
}
return res;
}
int main() {
cin >> n >> k; // 输入矩阵阶数 n 和幂次 k
mat a, res;
// 输入矩阵
for (int i = 0; i < n; i++) {
for (int j = 0; j < n; j++) {
cin >> a.m[i][j];
}
}
res = fastpow(a, k);
// 输出结果
for (int i = 0; i < n; i++) {
for (int j = 0; j < n; j++) {
cout << res.m[i][j] << " ";
}
cout << endl;
}
}
卢卡斯定理
https://blog.csdn.net/qq_40679299/article/details/80489761
代码思路
- 快速幂(二进制优化)
- 组合数(递归) C(n, m)
a[n]*pow(a[n-m],p-2)*pow(a[m],p-2) if(m > n) return 0 //不合法
- lucas lucas(n,m)
C(n%p, m%p)*lucas(n/p, m/p) if(m == 0) return 1
//卢卡斯定理
//解决组合数取模
#include <iostream>
#include <ctime>
#include <vector>
#include <cstdlib>
#include <sstream>
#include <algorithm>
using namespace std;
typedef long long ll;
const ll maxn = 100010;
ll n, m, p;
ll T;
ll a[maxn];
ll pow(ll y, ll z){ //快速幂
y %= p;
ll ans = 1;
for(int i = z; i > 0; i >>= 1){
if(i&1){
ans = ans*y%p;
}
y = y*y%p;
}
return ans;
}
ll C(ll n, ll m){ //定义组合数计算
if(m > n) return 0; //不合法
//
return ((a[n]*pow(a[m], p-2)) %p * pow(a[n-m], p-2) %p);
}
ll lucas(ll n, ll m){
if(m == 0) return 1;
return (C(n%p, m%p) * lucas(n/p, m/p) %p);
}
void test01()
{
cin >> T;
a[0] = 1;
for(ll i = 0; i < T; ++i){
cin >> n >> m >> p;
for(int j = 1; j <= p; ++j){
a[j] = (a[j-1]*j)%p; //求阶乘
}
cout << lucas(n+m, n) << endl; //求解C(n+m|n) mod p的值
}
}
int main()
{
test01();
return 0;
}
中国剩余定理
洛谷P3868
Blog
古题思路
- 找出三个数:从3和5的公倍数中找出被7除余1的最小数15,从3和7的公倍数中找出被5除余1的最小数21,最后从5和7的公倍数中找出除3余1的最小数70
- 用15乘以2(2为最终结果除以7的余数),用21乘以3(3为最终结果除以5的余数),同理,用70乘以2(2为最终结果除以3的余数),然后把三个乘积相加15∗2+21∗3+70∗2得到和233
- 用233除以3,5,7三个数的最小公倍数105,得到余数23,即233。这个余数23就是符合条件的最小数
代码思路
-
拓展GCD
-
求余数乘积lcm
-
遍历余数
- 求当前余数外余数乘积p
- 对该乘积和当前余数进行拓展GCD【得x,y】,取最小整数(+b[i])%b[i])
- ans += p * x * a[i]%lcm
-
前面有个非常关键的一个步骤,在初始化的时候要进行
#include<bits/stdc++.h>
#define ll long long
using namespace std;
ll a[110],b[110];//a:减数;b:余数
ll k;
//快读
inline int read()
{
int x=0,f=1;
char ch=getchar();
while(ch<'0'||ch>'9'){
if(ch=='-')
f=-1;
ch=getchar();
}
while(ch>='0'&&ch<='9'){
x=(x<<1)+(x<<3)+(ch^48);
ch=getchar();
}
return x*f;
}
//快速乘
inline ll qmul(ll a, ll b, ll m)
{
ll res=0;
while(b>0)
{
if(b&1)res=(res+a)%m;
a=(a+a)%m;
b>>=1;
}
return res;
}
//拓展GCD
inline void exgcd(ll a,ll b,ll &x,ll &y)
{
if(b==0)
{
x=1,y=0;
return;
}
exgcd(b,a%b,x,y);
int tmp=x;
x=y,y=tmp-a/b*y;
return;
}
//核心
ll China()
{
ll ans=0,lcm=1;
for(ll i=1;i<=k;i++)
lcm*=b[i]; //余数乘积为lcm
ll x,y;
for(ll i=1;i<=k;i++)
{
ll p=lcm/b[i];//其他余数的乘积
exgcd(p,b[i],x,y);//拓展GCD
x=(x%b[i]+b[i])%b[i];
//求出其他余数乘积*x
//再求上述乘积与减数的乘积
//ans+
ans=(ans+qmul(qmul(p,x,lcm),a[i],lcm))%lcm;
//ans += p * x * a[i]%lcm;
}
return (ans+lcm)%lcm;
}
int main()
{
cin>>k;
for(int i=1;i<=k;i++)
a[i]=read();
for(int i=1;i<=k;i++)
b[i]=read(),a[i]=(a[i]%b[i]+b[i])%b[i];
cout<<China();
return 0;
}
埃氏筛,判断素数(速度稍慢)
bool b[1001];//b数组判断是否为素数
void prime(){
for(i=2;i<=500;i++)
if(!b[i])
for(j=2;i*j<=1000;j++)
b[i*j]=1;
}//筛法
欧拉筛(最优)
代码思路
- 设置isprime数组,默认全部都是素数,1不是素数
- 2~n:
if(isprime[i]) prime[++cnt]=1;
没有筛掉,就是素数
//欧拉筛
#include <iostream>
#include <ctime>
#include <vector>
#include <cstdlib>
#include <sstream>
#include <algorithm>
#include <cstring>
using namespace std;
typedef long long ll;
const ll maxn = 1e8+9;
const ll maxq = 1e7+9;
ll n, q;
ll cnt = 0; //计数,记录prime数组的数据个数
ll prime[maxq]; //存放质数
bool isprime[maxn]; //=1表示是质数
void euler(ll n){ //欧拉筛,线性筛
memset(isprime, 1, sizeof(isprime)); //默认都是素数
isprime[1] = 0; //1不是素数
for(ll i = 2; i <= n; ++i){ //i遍历所有数据
if(isprime[i]){ //没筛掉
prime[++cnt] = i; //i成为下一个素数
}
for(ll j = 1; j <= cnt && i * prime[j] <= n; ++j){ //条件满足不超过上限
//从prime[1]为2开始,逐个枚举已知的质数,并期望Prime[j]是i*prime[j]的最小质因数
//当然,i肯定比prime[j]大,因为prime[j]在i之前得到
isprime[i*prime[j]] = 0;
if(i % prime[j] == 0){
//i中含有prime[j]因子
break;
}
}
}
}
void test01()
{
cin >> n >> q;
ll x;
euler(n);
for(ll i = 0; i < q; ++i){
cin >> x;
cout << prime[x] << endl;
}
}
int main()
{
test01();
return 0;
}
埃氏筛法的缺陷:对于一个合数,有可能被筛多次,浪费时间
③整数唯一分解定理:STL大法好
整数的唯一分解定理:一个大于1的整数一定可以被分解成若干质数的乘积。这个是数论知识,下面的代码基于这个定理,就不注释了
vector<int> k;
for(int a=2;a*a<=m;a++)
{
while(!(m%a))
{
k.push_back(a);
m=m/a;
}
}
if(m>1)
{
k.push_back(m);
}
return k;
唯一分解定理
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-cUlzZVmB-1649491696017)(C:\Users\Administrator\Desktop\123.png)]
分解因子
#include<iostream>
#include<cmath>
using namespace std;
const int N=1e5+100;
int n;
int main(){
cin>>n;
int p=sqrt(n);
int sum=0;
for(int i=1;i<=p;i++){
if(n%i==0){
sum+=i;
if(n/i!=i){
sum+=n/i;
}
}
}
if(sum==2*n){
cout<<"Y"<<endl;
} else cout<<"N"<<endl;
return 0;
}
高精度
#include<stdio.h>
#include<string>
#include<string.h>
#include<iostream>
using namespace std;
//compare比较函数:相等返回0,大于返回1,小于返回-1
int compare(string str1,string str2)
{
if(str1.length()>str2.length()) return 1;
else if(str1.length()<str2.length()) return -1;
else return str1.compare(str2);
}
//高精度加法
//只能是两个正数相加
string add(string str1,string str2)//高精度加法
{
string str;
int len1=str1.length();
int len2=str2.length();
//前面补0,弄成长度相同
if(len1<len2)
{
for(int i=1;i<=len2-len1;i++)
str1="0"+str1;
}
else
{
for(int i=1;i<=len1-len2;i++)
str2="0"+str2;
}
len1=str1.length();
int cf=0;
int temp;
for(int i=len1-1;i>=0;i--)
{
temp=str1[i]-'0'+str2[i]-'0'+cf;
cf=temp/10;
temp%=10;
str=char(temp+'0')+str;
}
if(cf!=0) str=char(cf+'0')+str;
return str;
}
//高精度减法
//只能是两个正数相减,而且要大减小
string sub(string str1,string str2)//高精度减法
{
string str;
int tmp=str1.length()-str2.length();
int cf=0;
for(int i=str2.length()-1;i>=0;i--)
{
if(str1[tmp+i]<str2[i]+cf)
{
str=char(str1[tmp+i]-str2[i]-cf+'0'+10)+str;
cf=1;
}
else
{
str=char(str1[tmp+i]-str2[i]-cf+'0')+str;
cf=0;
}
}
for(int i=tmp-1;i>=0;i--)
{
if(str1[i]-cf>='0')
{
str=char(str1[i]-cf)+str;
cf=0;
}
else
{
str=char(str1[i]-cf+10)+str;
cf=1;
}
}
str.erase(0,str.find_first_not_of('0'));//去除结果中多余的前导0
return str;
}
//高精度乘法
//只能是两个正数相乘
string mul(string str1,string str2)
{
string str;
int len1=str1.length();
int len2=str2.length();
string tempstr;
for(int i=len2-1;i>=0;i--)
{
tempstr="";
int temp=str2[i]-'0';
int t=0;
int cf=0;
if(temp!=0)
{
for(int j=1;j<=len2-1-i;j++)
tempstr+="0";
for(int j=len1-1;j>=0;j--)
{
t=(temp*(str1[j]-'0')+cf)%10;
cf=(temp*(str1[j]-'0')+cf)/10;
tempstr=char(t+'0')+tempstr;
}
if(cf!=0) tempstr=char(cf+'0')+tempstr;
}
str=add(str,tempstr);
}
str.erase(0,str.find_first_not_of('0'));
return str;
}
//高精度除法
//两个正数相除,商为quotient,余数为residue
//需要高精度减法和乘法
void div(string str1,string str2,string "ient,string &residue)
{
quotient=residue="";//清空
if(str2=="0")//判断除数是否为0
{
quotient=residue="ERROR";
return;
}
if(str1=="0")//判断被除数是否为0
{
quotient=residue="0";
return;
}
int res=compare(str1,str2);
if(res<0)
{
quotient="0";
residue=str1;
return;
}
else if(res==0)
{
quotient="1";
residue="0";
return;
}
else
{
int len1=str1.length();
int len2=str2.length();
string tempstr;
tempstr.append(str1,0,len2-1);
for(int i=len2-1;i<len1;i++)
{
tempstr=tempstr+str1[i];
tempstr.erase(0,tempstr.find_first_not_of('0'));
if(tempstr.empty())
tempstr="0";
for(char ch='9';ch>='0';ch--)//试商
{
string str,tmp;
str=str+ch;
tmp=mul(str2,str);
if(compare(tmp,tempstr)<=0)//试商成功
{
quotient=quotient+ch;
tempstr=sub(tempstr,tmp);
break;
}
}
}
residue=tempstr;
}
quotient.erase(0,quotient.find_first_not_of('0'));
if(quotient.empty()) quotient="0";
}
14. 博弈论
https://www.cnblogs.com/kannyi/p/8440359.html
巴什博弈
有n个物品,两个人轮流从这堆物品中取物,规定每次至少取一个,最多取m个,最后取光者得胜。
解法
n=(m+1)*k+c
若c!=0,先手必胜
否则先手必输
while(cin>>n>>m)
{
if(n%(m+1)==0)
cout<<"later win"<<endl;
else cout<<"earlier win"<<endl;
}
威佐夫博弈
有两堆若干数量的物品,两人轮流从其中一堆取至少一件物品,至多不限,或从两堆中同时取相同件物品,规定最后取完者胜利。
解法
若两堆物品的初始值分别为n和m,且n>m
[(√5+1)/2]*(n-m)=m时,后手必胜;否则先手必胜(左式取整)
while(cin>>n>>m)
{
if(m>n)
swap(n,m);
if(floor((n-m)*(1+sqrt(5.0))/2.0)==m)
cout<<"later win"<<endl;
else cout<<"earlier win"<<endl;
}
尼姆博弈
有任意堆物品,每堆物品的个数是任意的,双方轮流从中取物品,每一次只能从一堆物品中取部分或全部物品,最少取一件,取到最后一件物品的人获胜。
解法
把每堆物品数全部异或起来,即m = a1⊕a2⊕…⊕an,如果得到的值m为0,那么后手必胜,否则先手必胜。
while(cin>>n)
{
m=0;
for(i=0;i<n;i++)
{
cin>>a;
m=m^a;
}
if(m==0)
cout<<"later win"<<endl;
else cout<<"earlier win"<<endl;
}
斐波那契博弈
有一堆物品,数量为n个,两人轮流取物品,先手最少取一个,至多无上限,但不能把物品取完,之后每次取的物品数不能超过上一次取的物品数的二倍且至少为一件,取走最后一件物品的人获胜
解法
如果当n是斐波那契数时,后手必胜,否则先手必胜。
void Fib()
{
a[0]=0,a[1]=1;
for(int i=2;i<47;i++)
a[i]=a[i-1]+a[i-2];
}
int main()
{
Fib();
int n,i,flag;
while(cin>>n)
{
if(n==0)break;
flag=0;
for(i=0;i<MAX;i++)
{
if(a[i]==n)
{
flag=1;
break;
}
}
if(flag)
cout<<"later win"<<endl;
else cout<<"earlier win"<<endl;
}
return 0;
}
15. STL及其他相关的方便使用的API接口
https://blog.csdn.net/Bw9839/article/details/81054773
输入带空格字符串
string str; //Declare a string
getline(cin,str); //Input a line
int length = str.length();
reverse(str.begin(),str.end());
输入由end-of-file结束
string s;
while(cin >> s){
while(cin>>a){
if(cin.get()=='\n'){
break;
}
}
//输入一个a,如果监测到输入数据存在换行符’\n’则结束这个输入
min_element
(arr.begin(), arr.end()); //在arr.begin()到arr.end()的范围内找到最小的一个元素,并返回其迭代器。
next_permutation
全排列API,一定要先进行排序
二分API
lower_bound( begin,end,num):
从数组的begin位置到end-1位置二分查找第一个大于或等于num的数字,找到返回该数字的地址,不存在则返回end。通过返回的地址减去起始地址begin,得到找到数字在数组中的下标。
upper_bound( begin,end,num):
从数组的begin位置到end-1位置二分查找第一个大于num的数字,找到返回该数字的地址,不存在则返回end。通过返回的地址减去起始地址begin,得到找到数字在数组中的下标。
在从大到小的排序数组中,重载lower_bound()和upper_bound()
lower_bound( begin,end,num,greater() ):
从数组的begin位置到end-1位置二分查找第一个小于或等于num的数字,找到返回该数字的地址,不存在则返回end。通过返回的地址减去起始地址begin,得到找到数字在数组中的下标。
upper_bound( begin,end,num,greater() ):
从数组的begin位置到end-1位置二分查找第一个小于num的数字,找到返回该数字的地址,不存在则返回end。通过返回的地址减去起始地址begin,得到找到数字在数组中的下标。
binary_search:二分查找
sscanf
sscanf(line, "%d:%d:%d %d:%d:%d (+%d)", &h1, &m1, &s1, &h2, &m2, &s2, &d);
时间、字符串等操作
//航班时间
#include <cstdio>
#include <iostream>
#include <ctime>
#include <vector>
#include <cstdlib>
#include <sstream>
#include <algorithm>
#include <string.h>
using namespace std;
int n;
string s;
void test01()
{
(cin >> n).get();
for(int i = 0; i < n; ++i){
int h = 0, m = 0, s = 0;
//h1:m1:s1 h3:m3:s3 (+1)
for(int j = 0; j < 2; ++j){
int h1, h2, m1, m2, s1, s2, extra = 0;
char line[30];
cin.getline(line, 30);
if(strlen(line) == 17){
sscanf(line, "%d:%d:%d %d:%d:%d", &h1, &m1, &s1, &h2, &m2, &s2);
}else{
sscanf(line, "%d:%d:%d %d:%d:%d (+%d)", &h1, &m1, &s1, &h2, &m2, &s2, &extra);
}
h += extra*24+h2-h1;
m += m2-m1;
s += s2-s1;
}
if(h%2 != 0){
h--;
m += 60;
}
if(m%2 != 0){
m--;
s += 60;
}
h /= 2;
m /= 2;
s /= 2;
if(s < 0){
m--;
s += 60;
}
if(m < 0){
h--;
m += 60;
}
printf("%02d:%02d:%02d\n", h, m, s);
}
}
int main()
{
test01();
return 0;
}
快读算法
inline int read(){
int s=0,w=1;
char ch=getchar();
while(ch<'0'||ch>'9'){if(ch=='-')w=-1;ch=getchar();}
while(ch>='0'&&ch<='9') s=s*10+ch-'0',ch=getchar();
return s*w;
}
inline int read(){
char c=getchar();
int x=0;
while(c<'0'||c>'9') c=getchar();
while(c>='0'&&c<='9') x=(x<<3)+(x<<1)+c-'0',c=getchar();
return x;
}
string
map
5,插入元素
// 定义一个map对象
map<int, string> mapStudent;
// 第一种 用insert函數插入pair
mapStudent.insert(pair<int, string>(000, "student_zero"));
// 第二种 用insert函数插入value_type数据
mapStudent.insert(map<int, string>::value_type(001, "student_one"));
// 第三种 用"array"方式插入
mapStudent[123] = "student_first";
mapStudent[456] = "student_second";
以上三种用法,虽然都可以实现数据的插入,但是它们是有区别的,当然了第一种和第二种在效果上是完成一样的,用insert函数插入数据,在数据的 插入上涉及到集合的唯一性这个概念,即当map中有这个关键字时,insert操作是不能在插入数据的,但是用数组方式就不同了,它可以覆盖以前该关键字对 应的值,用程序说明如下:
mapStudent.insert(map<int, string>::value_type (001, "student_one"));
mapStudent.insert(map<int, string>::value_type (001, "student_two"));
上面这两条语句执行后,map中001这个关键字对应的值是“student_one”,第二条语句并没有生效,那么这就涉及到我们怎么知道insert语句是否插入成功的问题了,可以用pair来获得是否插入成功,程序如下
// 构造定义,返回一个pair对象
pair<iterator,bool> insert (const value_type& val);
pair<map<int, string>::iterator, bool> Insert_Pair;
Insert_Pair = mapStudent.insert(map<int, string>::value_type (001, "student_one"));
if(!Insert_Pair.second)
cout << ""Error insert new element" << endl;
我们通过pair的第二个变量来知道是否插入成功,它的第一个变量返回的是一个map的迭代器,如果插入成功的话Insert_Pair.second应该是true的,否则为false。
6, 查找元素
当所查找的关键key出现时,它返回数据所在对象的位置,如果沒有,返回iter与end函数的值相同。
// find 返回迭代器指向当前查找元素的位置否则返回map::end()位置
iter = mapStudent.find("123");
if(iter != mapStudent.end())
cout<<"Find, the value is"<<iter->second<<endl;
else
cout<<"Do not Find"<<endl;
7, 刪除与清空元素
//迭代器刪除
iter = mapStudent.find("123");
mapStudent.erase(iter);
//用关键字刪除
int n = mapStudent.erase("123"); //如果刪除了會返回1,否則返回0
//用迭代器范围刪除 : 把整个map清空
mapStudent.erase(mapStudent.begin(), mapStudent.end());
//等同于mapStudent.clear()
8,map的大小
在往map里面插入了数据,我们怎么知道当前已经插入了多少数据呢,可以用size函数,用法如下:
int nSize = mapStudent.size()
C++ maps是一种关联式容器,包含“关键字/值”对
begin() 返回指向map头部的迭代器
clear() 删除所有元素
count() 返回指定元素出现的次数
empty() 如果map为空则返回true
end() 返回指向map末尾的迭代器
equal_range() 返回特殊条目的迭代器对
erase() 删除一个元素
find() 查找一个元素
get_allocator() 返回map的配置器
insert() 插入元素
key_comp() 返回比较元素key的函数
lower_bound() 返回键值>=给定元素的第一个位置
max_size() 返回可以容纳的最大元素个数
rbegin() 返回一个指向map尾部的逆向迭代器
rend() 返回一个指向map头部的逆向迭代器
size() 返回map中元素的个数
swap() 交换两个map
upper_bound() 返回键值>给定元素的第一个位置
value_comp() 返回比较元素value的函数
multiset
multiset是set库中一个非常有用的类型,它可以看成一个序列,插入一个数,删除一个数都能够在O(logn)的时间内完成
而且他能时刻保证序列中的数是有序的,而且序列中可以存在重复的数
multiset<int>q;
//定义一个multiset,尖括号里写类型
//如果是自定义类型,需要重载小于号
q.insert(x);
//插入一个数 x
q.clear();
//清空
q.erase(x);
//删除容器中的所有值为 x 的数
q.erase(it);
//删除容器中迭代器it指向的元素
q.empty();
//返回bool值,如果容器为空返回true,否则返回false
q.size()
//返回元素个数
q.begin();
//返回首个元素的迭代器
q.end();
//返回最后一个元素的下一个位置的迭代器
q.count(x);
//返回容器中 x 的个数
q.find(x);
//返回容器中第一个x的位置(迭代器),如果没有就返回q.end()
q.lower_bound(x);
//返回容器中第一个大于等于x的数的迭代器
q.upper_bound(x);
//返回容器中第一个大于x的数的迭代器
}
15. STL及其他相关的方便使用的API接口
https://blog.csdn.net/Bw9839/article/details/81054773
输入带空格字符串
string str; //Declare a string
getline(cin,str); //Input a line
int length = str.length();
reverse(str.begin(),str.end());
输入由end-of-file结束
string s;
while(cin >> s){
while(cin>>a){
if(cin.get()=='\n'){
break;
}
}
//输入一个a,如果监测到输入数据存在换行符’\n’则结束这个输入
min_element
(arr.begin(), arr.end()); //在arr.begin()到arr.end()的范围内找到最小的一个元素,并返回其迭代器。
next_permutation
全排列API,一定要先进行排序
二分API
lower_bound( begin,end,num):
从数组的begin位置到end-1位置二分查找第一个大于或等于num的数字,找到返回该数字的地址,不存在则返回end。通过返回的地址减去起始地址begin,得到找到数字在数组中的下标。
upper_bound( begin,end,num):
从数组的begin位置到end-1位置二分查找第一个大于num的数字,找到返回该数字的地址,不存在则返回end。通过返回的地址减去起始地址begin,得到找到数字在数组中的下标。
在从大到小的排序数组中,重载lower_bound()和upper_bound()
lower_bound( begin,end,num,greater() ):
从数组的begin位置到end-1位置二分查找第一个小于或等于num的数字,找到返回该数字的地址,不存在则返回end。通过返回的地址减去起始地址begin,得到找到数字在数组中的下标。
upper_bound( begin,end,num,greater() ):
从数组的begin位置到end-1位置二分查找第一个小于num的数字,找到返回该数字的地址,不存在则返回end。通过返回的地址减去起始地址begin,得到找到数字在数组中的下标。
binary_search:二分查找
sscanf
sscanf(line, "%d:%d:%d %d:%d:%d (+%d)", &h1, &m1, &s1, &h2, &m2, &s2, &d);
时间、字符串等操作
//航班时间
#include <cstdio>
#include <iostream>
#include <ctime>
#include <vector>
#include <cstdlib>
#include <sstream>
#include <algorithm>
#include <string.h>
using namespace std;
int n;
string s;
void test01()
{
(cin >> n).get();
for(int i = 0; i < n; ++i){
int h = 0, m = 0, s = 0;
//h1:m1:s1 h3:m3:s3 (+1)
for(int j = 0; j < 2; ++j){
int h1, h2, m1, m2, s1, s2, extra = 0;
char line[30];
cin.getline(line, 30);
if(strlen(line) == 17){
sscanf(line, "%d:%d:%d %d:%d:%d", &h1, &m1, &s1, &h2, &m2, &s2);
}else{
sscanf(line, "%d:%d:%d %d:%d:%d (+%d)", &h1, &m1, &s1, &h2, &m2, &s2, &extra);
}
h += extra*24+h2-h1;
m += m2-m1;
s += s2-s1;
}
if(h%2 != 0){
h--;
m += 60;
}
if(m%2 != 0){
m--;
s += 60;
}
h /= 2;
m /= 2;
s /= 2;
if(s < 0){
m--;
s += 60;
}
if(m < 0){
h--;
m += 60;
}
printf("%02d:%02d:%02d\n", h, m, s);
}
}
int main()
{
test01();
return 0;
}
快读算法
inline int read(){
int s=0,w=1;
char ch=getchar();
while(ch<'0'||ch>'9'){if(ch=='-')w=-1;ch=getchar();}
while(ch>='0'&&ch<='9') s=s*10+ch-'0',ch=getchar();
return s*w;
}
inline int read(){
char c=getchar();
int x=0;
while(c<'0'||c>'9') c=getchar();
while(c>='0'&&c<='9') x=(x<<3)+(x<<1)+c-'0',c=getchar();
return x;
}
string
map
5,插入元素
// 定义一个map对象
map<int, string> mapStudent;
// 第一种 用insert函數插入pair
mapStudent.insert(pair<int, string>(000, "student_zero"));
// 第二种 用insert函数插入value_type数据
mapStudent.insert(map<int, string>::value_type(001, "student_one"));
// 第三种 用"array"方式插入
mapStudent[123] = "student_first";
mapStudent[456] = "student_second";
以上三种用法,虽然都可以实现数据的插入,但是它们是有区别的,当然了第一种和第二种在效果上是完成一样的,用insert函数插入数据,在数据的 插入上涉及到集合的唯一性这个概念,即当map中有这个关键字时,insert操作是不能在插入数据的,但是用数组方式就不同了,它可以覆盖以前该关键字对 应的值,用程序说明如下:
mapStudent.insert(map<int, string>::value_type (001, "student_one"));
mapStudent.insert(map<int, string>::value_type (001, "student_two"));
上面这两条语句执行后,map中001这个关键字对应的值是“student_one”,第二条语句并没有生效,那么这就涉及到我们怎么知道insert语句是否插入成功的问题了,可以用pair来获得是否插入成功,程序如下
// 构造定义,返回一个pair对象
pair<iterator,bool> insert (const value_type& val);
pair<map<int, string>::iterator, bool> Insert_Pair;
Insert_Pair = mapStudent.insert(map<int, string>::value_type (001, "student_one"));
if(!Insert_Pair.second)
cout << ""Error insert new element" << endl;
我们通过pair的第二个变量来知道是否插入成功,它的第一个变量返回的是一个map的迭代器,如果插入成功的话Insert_Pair.second应该是true的,否则为false。
6, 查找元素
当所查找的关键key出现时,它返回数据所在对象的位置,如果沒有,返回iter与end函数的值相同。
// find 返回迭代器指向当前查找元素的位置否则返回map::end()位置
iter = mapStudent.find("123");
if(iter != mapStudent.end())
cout<<"Find, the value is"<<iter->second<<endl;
else
cout<<"Do not Find"<<endl;
7, 刪除与清空元素
//迭代器刪除
iter = mapStudent.find("123");
mapStudent.erase(iter);
//用关键字刪除
int n = mapStudent.erase("123"); //如果刪除了會返回1,否則返回0
//用迭代器范围刪除 : 把整个map清空
mapStudent.erase(mapStudent.begin(), mapStudent.end());
//等同于mapStudent.clear()
8,map的大小
在往map里面插入了数据,我们怎么知道当前已经插入了多少数据呢,可以用size函数,用法如下:
int nSize = mapStudent.size()
C++ maps是一种关联式容器,包含“关键字/值”对
begin() 返回指向map头部的迭代器
clear() 删除所有元素
count() 返回指定元素出现的次数
empty() 如果map为空则返回true
end() 返回指向map末尾的迭代器
equal_range() 返回特殊条目的迭代器对
erase() 删除一个元素
find() 查找一个元素
get_allocator() 返回map的配置器
insert() 插入元素
key_comp() 返回比较元素key的函数
lower_bound() 返回键值>=给定元素的第一个位置
max_size() 返回可以容纳的最大元素个数
rbegin() 返回一个指向map尾部的逆向迭代器
rend() 返回一个指向map头部的逆向迭代器
size() 返回map中元素的个数
swap() 交换两个map
upper_bound() 返回键值>给定元素的第一个位置
value_comp() 返回比较元素value的函数
multiset
multiset是set库中一个非常有用的类型,它可以看成一个序列,插入一个数,删除一个数都能够在O(logn)的时间内完成
而且他能时刻保证序列中的数是有序的,而且序列中可以存在重复的数
multiset<int>q;
//定义一个multiset,尖括号里写类型
//如果是自定义类型,需要重载小于号
q.insert(x);
//插入一个数 x
q.clear();
//清空
q.erase(x);
//删除容器中的所有值为 x 的数
q.erase(it);
//删除容器中迭代器it指向的元素
q.empty();
//返回bool值,如果容器为空返回true,否则返回false
q.size()
//返回元素个数
q.begin();
//返回首个元素的迭代器
q.end();
//返回最后一个元素的下一个位置的迭代器
q.count(x);
//返回容器中 x 的个数
q.find(x);
//返回容器中第一个x的位置(迭代器),如果没有就返回q.end()
q.lower_bound(x);
//返回容器中第一个大于等于x的数的迭代器
q.upper_bound(x);
//返回容器中第一个大于x的数的迭代器