自用算法笔记(持续更新)

本文原载自我的个人博客

1.第一章 基础算法

时间复杂度

重视思想

好的方法 课下 模板(重复3-5次)思想理解会背 然后做题目

根据数据量猜解法

常数指令 一次a+b ,i ++这种就是,假如给你一个1e6长度的数组,用n^2的算法的话,数据量就成了1e12,我们设计的算法肯定不能超过1e7~1e8,假如用n*log2 n ,算下来也就是1e7多一点,未必能超,也许能行。假如是1e3长度,那么n2也行。

注意:上面使用技巧的必要条件↑

快速排序

用这个就行,防止边界问题

归并排序

思想也是分治,但是方法不一样

先划分区间,再合并

最后那个for是把temp拷贝回去 temp是记录递归排序的数组,每一步都在改变,不能最后直接输出temp

C++

#include<iostream>
using namespace std;
int a[100010];
int t[100010];
/*void merge_sort(int a[],int l,int r){
    if(l == r)return;
    int mid = l + r >> 1;
    merge_sort(a,l,mid),merge_sort(a,mid+1,r);
    for(int i = l,j = l, k = mid + 1; i <= r; i ++){
        if(j == mid + 1) 
            t[i] = a[k ++];
        else if(k == r + 1)
            t[i] = a[j ++];
        else t[i] = a[j] < a[k] ? a[j ++] : a[k ++];
    }
    for(int i = l; i <= r; i ++)a[i]=t[i];
}*/
void merge_sort(int a[], int l, int r){
    //只剩一个元素的时候,已经有序,返回
    if(l >= r) return;

    //寻找数组中点下标
    int mid = (l+r)>>1;
    //递归给左半边排序
    merge_sort(a,l,mid);
    //递归给右半边排序
    merge_sort(a,mid+1,r);
    //以下是合并排序好的两个数组

    //k:遍历合并后的数组的下标
    int k = 0;
    //i:左半边数组的下标,j:右半边数组的下标
    int i = l, j = mid + 1;
    //左右半边都没遍历完
    while(i <= mid && j <= r){
        //左边的元素小于右边的元素
        if(a[i] < a[j]) 
            //左边元素放如临时数组,并移动下标
            temp[k++] = a[i++];
        //否则,右边元素放入临时数组并移动下标 
        else temp[k++] = a[j++];
    }
    //如果左边数组有剩余,则放入临时数组
    while(i <= mid) temp[k++] = a[i++];
    //如果有边数组有剩余,则放入临时数组
    while(j <= r) temp[k++] = a[j++];
    //把临时数组中的元素拷贝至原数组
    k = 0;
    for(int i = l; i <= r; i++){
        a[i] = temp[k++];
    }

}


int main(){
    int n;
    scanf("%d",&n);
    for(int i = 0 ; i < n; i ++){
        scanf("%d",&a[i]);
    }
    merge_sort(a,0,n-1);
    for(int i = 0 ; i < n; i ++){
        printf("%d ",a[i]);
    }
}
例题 逆序对

https://www.acwing.com/solution/content/5103/讲解很好

分治

计数排序

桶排序

例题洛谷P7020,适用情况,要排序的数字值域小,但是n很大的情况,sort和快排这些适合值域大小都可以,n小点的情况

二分

C++

没有单调性也可能二分(本质不是单调性)

整数二分算法模板 —— 模板题 AcWing 789. 数的范围
bool check(int x) {/* ... */} // 检查x是否满足某种性质

// 区间[l, r]被划分成[l, mid]和[mid + 1, r]时使用:
int bsearch_1(int l, int r)
{
    while (l < r)
    {
        int mid = l + r >> 1;
        if (check(mid)) r = mid;    // check()判断mid是否满足性质
        else l = mid + 1;
    }
    return l;
}
// 区间[l, r]被划分成[l, mid - 1]和[mid, r]时使用:
int bsearch_2(int l, int r)
{
    while (l < r)
    {
        int mid = l + r + 1 >> 1;
        if (check(mid)) l = mid;
        else r = mid - 1;
    }
    return l;
}

浮点数二分算法模板 —— 模板题 AcWing 790. 数的三次方根
bool check(double x) {/* ... */} // 检查x是否满足某种性质

double bsearch_3(double l, double r)
{
    const double eps = 1e-6;   // eps 表示精度,取决于题目对精度的要求
    while (r - l > eps)
    {
        double mid = (l + r) / 2;
        if (check(mid)) r = mid;
        else l = mid;
    }
    return l;
}

想清楚性质 边界

无解和二分无关,可以用二分出的结果判断原题有没有解

浮点数二分更简单,l和r距离足够小就可以认为是个数了

而且不用考虑向上向下取整 直接/2

浮点数二分算法模板 —— 模板题 AcWing 790. 数的三次方根

C++

bool check(double x) {/* ... */} // 检查x是否满足某种性质

double bsearch_3(double l, double r)
{
    const double eps = 1e-6;   // eps 表示精度,取决于题目对精度的要求
    while (r - l > eps)
    {
        double mid = (l + r) / 2;
        if (check(mid)) r = mid;
        else l = mid;
    }
    return l;
}

有经验,就是假如题目要求4位的话,最好-6,就是比要求的多两位,保证没问题

例题

二分答案(10.15-10.17 11.13)
关于lower_bound( )和upper_bound( )的常见用法

高精度(略过)

前缀和 和 差分

一维前缀和 —— 模板题 AcWing 795. 前缀和
S[i] = a[1] + a[2] + ... a[i]
a[l] + ... + a[r] = S[r] - S[l - 1]

二维前缀和 —— 模板题 AcWing 796. 子矩阵的和

S[i, j] = 第i行j列格子左上部分所有元素的和
以(x1, y1)为左上角,(x2, y2)为右下角的子矩阵的和为:
S[x2, y2] - S[x1 - 1, y2] - S[x2, y1 - 1] + S[x1 - 1, y1 - 1]

一维差分 —— 模板题 AcWing 797. 差分
给区间[l, r]中的每个数加上c:B[l] += c, B[r + 1] -= c

先得到差分数组

二维差分 —— 模板题 AcWing 798. 差分矩阵
给以(x1, y1)为左上角,(x2, y2)为右下角的子矩阵中的所有元素加上c:
S[x1, y1] += c, S[x2 + 1, y1] -= c, S[x1, y2 + 1] -= c, S[x2 + 1, y2 + 1] += c

这个讲解很好AcWing 798. 差分矩阵 【 c++详细题解 】 - AcWing

位运算(待更)

离散化

如果数据范围小的话(10的五次方以内)用前缀和也可

C++用vector作离散化

下面是去重函数的用法

https://blog.csdn.net/tjcwt2011/article/details/125281748?ops_request_misc=%257B%2522request%255Fid%2522%253A%2522169275072116800184115652%2522%252C%2522scm%2522%253A%252220140713.130102334..%2522%257D&request_id=169275072116800184115652&biz_id=0&utm_medium=distribute.pc_search_result.none-task-blog-2~all~baidu_landing_v2~default-1-125281748-null-null.142^v93^chatgptT3_1&utm_term=C%2B%2B%E5%8E%BB%E9%87%8D%E5%87%BD%E6%95%B0&spm=1018.2226.3001.4187

去重完用sort排序最后用二分查找(好像是先排序后去重

二分函数lower_bound(大于等于) upper_bound(大于)

lower_bound()/upper_bound()函数(C++)_c++ lowerbound upperbound-CSDN博客

https://www.acwing.com/solution/content/80100/这个代码解释非常详细

csdn的这个博客也很不错(11.11)

离散化算法-CSDN博客

区间合并

好多端点问题都是贪心

https://www.acwing.com/solution/content/2615/更新维护区间(很重要)

2.第二章 数据结构

链表和邻接表

单链表

用的最多的是邻接表

(存储图和树)

AcWing 826. 单链表 - AcWing

双链表

添加的顺序不能反

邻接表

栈和队列

表达式求值(中缀 后缀)一般是二元(不包括符号)

单调栈(2023.12.15)

例题洛谷P2866

C++

#include<bits/stdc++.h>
using namespace std;
stack<int> st;
int n;
int main(){
	cin >> n;
	long long ans= 0; 
	long long cnt = 0;
	for(int i = 1 ; i <=n ; i ++){
		int x;
		cin >> x;	
		while(!st.empty()&&st.top()<=x){
				st.pop();
				cnt--; 
			}
			ans+=cnt;
			st.push(x);
			cnt++;
		}
	}
	cout << ans;
	return 0;
}

单调队列

用STL的双端队列deque非常方便

例题 洛谷P2032

C++

#include<bits/stdc++.h>
using namespace std;
const int N = 2e6 + 10;
struct node{int v,id;}a[N];
deque<node> q;
int n,k;
int main(){
	cin >> n >>k;
	for(int i= 1; i <= n; i ++){
		cin >> a[i].v;
		a[i].id = i;
	}
	for(int i = 1; i <= n; i ++){
		while(!q.empty()&&q.back().v<a[i].v){
			q.pop_back();
		}
		q.push_back(a[i]);
		if(q.front().id==i-k)q.pop_front();
		if(i>=k)cout << q.front().v<<endl;
	}
	return 0;
}

虽然抽象但是题型固定就那几个

AcWing 830. 单调栈--图解,详细注释 - AcWing 这个非常易于适合理解单调栈

单调栈:见博客

单调队列:见博客https://blog.csdn.net/qq_50285142/article/details/120245122?ops_request_misc=%257B%2522request%255Fid%2522%253A%2522169761847116800180675961%2522%252C%2522scm%2522%253A%252220140713.130102334..%2522%257D&request_id=169761847116800180675961&biz_id=0&utm_medium=distribute.pc_search_result.none-task-blog-2~all~top_positive~default-1-120245122-null-null.142^v96^pc_search_result_base1&utm_term=%E5%8D%95%E8%B0%83%E9%98%9F%E5%88%97&spm=1018.2226.3001.4187

kmp算法(略)

Trie

题一般字符串字母类型少

算法竞赛里的trie树最多26个或者52个字母

并查集(12.8)

https://leetcode.cn/circle/discuss/qmjuMW/

堆(12.19)

堆排序算法(图解详细流程)_堆排序的详细过程-CSDN博客

AcWing 838. 堆排序--海绵宝宝来喽 - AcWing

堆排序:

从最后一个非叶子节点开始

向上调整

向下调整

哈希表

字符串哈希适用场景:不考虑冲突,不能映射成0

字符串前缀哈希值

树状数组

例题 动态求区间和
树状数组

CPP

#include<bits/stdc++.h>

using namespace std;

const int N=100009;

int a[N],tr[N];

int n,m;

//每个数的间隔,背下来就行
int lowbit(int x)
{
    return x&-x;
}

//第x个数加上v
int add(int x,int v)
{
    //因为树状数组的性质,加一个数,只影响logn个数,所有不用全加完
    //从当前位置开始加,每个间隔是lowbit(i),一直加到最后
    for(int i=x;i<=n;i+=lowbit(i))
        tr[i]+=v;
}

//返回x的前缀和
int qurry(int x)
{
    //因为树状数组的性质,求前缀和,只用加logn个数,所有不用全加完
    //从当前位置开始累加,每个间隔是lowbit(i),一直加到i==0停止
    int cnt=0;
    for(int i=x;i!=0;i-=lowbit(i)){
        cnt+=tr[i];
    }
    return cnt;

}

int main()
{
    cin>>n>>m;
    for(int i=1;i<=n;i++)
        scanf("%d",&a[i]);
    for(int i=1;i<=n;i++)
        add(i,a[i]);//第i个数加上a[i]

while(m--)
{
    int k,x,y;
    scanf("%d%d%d",&k,&x,&y);
    if(k==0) printf("%d\n",qurry(y)-qurry(x-1));
    else add(x,y);
}
return 0;

}
这道题的线段树方法

4倍空间的原因

CPP

#include<iostream>
#include<cstring>
#include<cstdio>
#include<algorithm>

using namespace std;

const int N=100010;

int n,m;

int w[N];//记录一下权重

struct node{
    int l,r;//左右区间

    int sum;//总和
}tr[N*4];//记得开 4 倍空间 


void push_up(int u)//利用它的两个儿子来算一下它的当前节点信息
{
    tr[u].sum=tr[u<<1].sum+tr[u<<1|1].sum;//左儿子 u<<1 ,右儿子 u<<1|1  
}

void build(int u,int l,int r)/*第一个参数,当前节点编号,第二个参数,左边界,第三个参数,右边界*/
{
    if(l==r)tr[u]={l,r,w[r]};//如果当前已经是叶节点了,那我们就直接赋值就可以了
    else//否则的话,说明当前区间长度至少是 2 对吧,那么我们需要把当前区间分为左右两个区间,那先要找边界点
    {
        tr[u]={l,r};//这里记得赋值一下左右边界的初值

        int mid=l+r>>1;//边界的话直接去计算一下 l + r 的下取整

        build(u<<1,l,mid);//先递归一下左儿子

        build(u<<1|1,mid+1,r);//然后递归一下右儿子

        push_up(u);//做完两个儿子之后的话呢 push_up 一遍u 啊,更新一下当前节点信息
    }
}

int query(int u,int l,int r)//查询的过程是从根结点开始往下找对应的一个区间
{
    if(l<=tr[u].l&&tr[u].r<=r)return tr[u].sum;//如果当前区间已经完全被包含了,那么我们直接返回它的值就可以了
    //否则的话我们需要去递归来算
    int mid=tr[u].l+tr[u].r>>1;//计算一下我们 当前 区间的中点是多少
    //先判断一下和左边有没有交集

    int sum=0;//用 sum 来表示一下我们的总和

    if(mid>=l)sum+=query(u<<1,l,r);//看一下我们当前区间的中点和左边有没有交集
    if(r>=mid+1)//看一下我们当前区间的中点和右边有没有交集
    sum+=query(u<<1|1,l,r);

    return sum;

}

void modify(int u,int x,int v)//第一个参数也就是当前节点的编号,第二个参数是要修改的位置,第三个参数是要修改的值
{
    if(tr[u].l==tr[u].r)tr[u].sum+=v; //如果当前已经是叶节点了,那我们就直接让他的总和加上 v 就可以了

    //否则
    else
    {

      int mid=tr[u].l+tr[u].r>>1;
      //看一下 x 是在左半边还是在右半边
      if(x<=mid)modify(u<<1,x,v);//如果是在左半边,那就找左儿子
      else modify(u<<1|1,x,v);//如果在右半边,那就找右儿子

      //更新完之后当前节点的信息就要发生变化对吧,那么我们就需要 pushup 一遍

      push_up(u);
    }

}

int main()
{
    scanf("%d%d",&n,&m);

    for(int i=1;i<=n;i++)scanf("%d",&w[i]);

    build(1,1,n);/*第一个参数是根节点的下标,根节点是一号点,然后初始区间是 1 到 n */

    //后面的话就是一些修改操作了

    while(m--)
    {
        int k,a,b;

        scanf("%d%d%d",&k,&a,&b);

        if(!k)printf("%d\n",query(1,a,b));//求和的时候,也是传三个参数,第一个的话是根节点的编号 ,第二个的话是我们查询的区间 
        //第一个参数也就是当前节点的编号
        else
        modify(1,a,b);//第一个参数是根节点的下标,第二个参数是要修改的位置,第三个参数是要修改的值

    }


    return 0;
}

作者:Elegant
链接:https://www.acwing.com/solution/content/40394/
来源:AcWing
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
区间求和例题:洛谷P3368

CPP

#include<bits/stdc++.h>
using namespace std;
const int N = 5e5 + 10;
int a[N];
int b[N];
int tr[N];
int n,m;
int lowbit(int x){
	return x & -x;
}
void add(int k,int c){
	for(int i = k; i <= n; i += lowbit(i)){
		tr[i] += c;
	}
}
int query(int x){
	int res = 0;
	for(int i = x; i > 0; i -= lowbit(i)){
		res += tr[i];
	}	
	return res;
}
int main(){
	cin >> n >> m;
	for(int i = 1; i <= n; i ++){
		cin >> a[i];
		b[i] = a[i] - a[i - 1];
		add(i,b[i]);
	}
	while(m --){
		int op;
		cin >> op;
		if(op == 1){
			int x,y,k;
			cin >> x >> y >> k;
			add(x,k);
			add(y + 1,-k);
		}else{
			int x;
			cin >> x;
			cout << query(x) << endl;
		}
	}
}

线段树(待更)

3.第三章 搜索与图论

深度优先搜索(DFS)

两个很重要的概念:回溯 剪枝

每个DFS都一定对应一个搜索树

AcWing 842. 排列数字--深度优先遍历代码+注释 - AcWing

** 经典题目**

八皇后

广度优先搜索(BFS)

不是所有最短路都用BFS,只能当边权都是1时,一般情况都是用最短路算法

BFS函数里一般都用队列

树与图的存储(10.21)

【AgOHの数据结构】你真的了解链式前向星吗?_哔哩哔哩_bilibili

这个链式前向星讲的特别好↑

树是特殊的图,所以只讲图就行

图:有向图,无向图,边有无方向,无向图可以看作特殊的有向图,所以只要看有向图

有向图(a->b):

邻接矩阵g[a] [b] a->b,有重边保留一条就可以了

邻接表

树与图的深度优先遍历

image-20231204141022311

**https://leetcode.cn/circle/discuss/FyPTTM/**宝藏

讲解了为什么要反向建边 [洛谷日报第85期]图论的小技巧以及扩展_网易订阅

图的遍历 - 洛谷

洛谷这个题↑正向遍历超时了 n 1e5 ,用反向遍历更适合

思路真妙,把大的先走完,打上标记,肯定就是最大的了

图的讲解(2023.12.4很好) 登录 - Luogu Spilopelia

这个邻接表写法nice↓

CPP

#include <iostream>
#include <vector>
#include <queue>
#include <cstring>
#include <algorithm>
using namespace std;

const int MAXN = 100010;

std::vector<int> G[MAXN];
int n, m;
bool visited[MAXN];
queue<int> q;

void dfs(int x, int cur) {//x指当前所在的节点,cur指已遍历过的节点个数
	visited[x] = true;//标记以避免重复访问
	cout << x << " ";//输出
	if (cur == n) return ;
	for (int i=0; i<G[x].size(); i++)
		if (!visited[G[x][i]]) dfs(G[x][i], cur+1);//记得要判断是否遍历过
}

void bfs(int x) {
	memset(visited, false, sizeof(visited));//记得一定要清空
	visited[x] = true;
	q.push(x);
	while (!q.empty()) {
		int v = q.front();
		q.pop();//记得要弹出,否则会一直在第一层遍历
		cout << v << " ";//输出
		for (int i=0; i<G[v].size(); i++) 
			if (!visited[G[v][i]]) {
				visited[G[v][i]] = true;
				q.push(G[v][i]);//记得要入队
			}
	}
}

int main() {
	cin >> n >> m;
	for (int i=1; i<=m; i++) {
		int u, v;
		cin >> u >> v;
		G[u].push_back(v);//标准邻接表建有向图
	}
	for (int i=1; i<=n; i++) sort(G[i].begin(), G[i].end());//标准vector排序
	dfs(1, 0);
	cout << endl;
	bfs(1);
	cout << endl;
	return 0;//完结撒花!
}

非常重要!!!由前+中/后+中构建序列 模板(字符串切割 + 练习STL substr)

登录 - Luogu Spilopelia

C++

由中序和前序求后序
#include<bits/stdc++.h>
using namespace std;
string pre,inor;
void dfs(string pre,string inor){
    if(pre.empty())return;
    char root = pre[0];
    int k = inor.find(root);
    pre.erase(pre.begin());
    string leftpre = pre.substr(0,k);
    string rightpre = pre.substr(k);
    string leftinor = inor.substr(0,k);
    string rightinor = inor.substr(k + 1);
    dfs(leftpre,leftinor);
    dfs(rightpre,rightinor);
    printf("%c",root);

}
int main(){
    cin >> inor >>pre ;
    dfs(pre,inor);
    putchar('\n');
    return 0;
}

[NOIP2001 普及组] 求先序排列 - 洛谷

C++

由中序和后序求前序
#include<bits/stdc++.h>
using namespace std;
string inor,post;
void solve(string inor,string post){
    if(post.empty())return;
    char root = post[post.size() - 1];
    post.erase(post.end()-1);
    int k = inor.find(root);
    
    string inorleft = inor.substr(0,k);
    string inorright= inor.substr(k + 1);
    string postleft = post.substr(0,k);
    string postright = post.substr(k);
    cout << root;
    solve(inorleft,postleft);
    solve(inorright,postright);

}
int main(){
    cin >> inor >> post;
    solve(inor,post);
    return 0;
}

CPP

#include<string>
#include<cstring>
#include<iostream>
#include<cstdio>
using namespace std;
string pre,inor;
void work(string pre,string inor)
{
    if(pre.empty())return;
    //如果序列空了,就没必要继续了
    char root=pre[0];
    //取到前序序列的首字母,即根节点
    int k=inor.find(root);
    //找到中序序列中根节点的位置
    pre.erase(pre.begin());
    //删去前序序列中的根节点
    string leftpre=pre.substr(0,k);
    //从0开始切割k个
    string rightpre=pre.substr(k);
    //从k开始切割到最后
    string leftinor=inor.substr(0,k);
    //从0开始切割k个
    string rightinor=inor.substr(k+1);
    //从k+1开始切割到最后
    work(leftpre,leftinor);
    work(rightpre,rightinor);
    printf("%c",root);
    //因为要输出后序序列,所以是左右根
    //先遍历左子树,再右子树,再根节点
}
int main()
{
    cin>>inor>>pre;
    work(pre,inor);
    putchar('\n');
    return 0;
}

树与图的宽度优先遍历(待更)

DAG() 与 拓扑排序(12.4)

知乎的讲解,很好↓[知识点]

图文详解面试常考算法 —— 拓扑排序 - 知乎

拓扑排序邻接表版[代码]

活动 - AcWing

C++

#include<bits/stdc++.h>
using namespace std;
const int N = 1e5 + 10;
vector<int> G[N];//邻接表 
queue<int> q;//队列操作 
int d[N];//统计入度 
int n,m,cnt,ans[N];//ans数组记录答案 
int main(){
	cin >> n >> m;
	for(int i = 1 ;i <= m; i ++){
		int x,y;
		cin >> x>> y;
		G[x].push_back(y);
		d[y]++;//统计入度 
	}
	for(int i =1; i <= n; i ++){
		if(d[i]==0)q.push(i);//1
	}
	while(q.size()){
		int t = q.front();
		q.pop();
		ans[cnt++] = t;
		for(int i = 0 ; i < G[t].size();i  ++){
			d[G[t][i]]--;//删边操作 
			if(d[G[t][i]]==0) q.push(G[t][i]);//删完后入度为0的话,放入队列 
		}
	}
	if(cnt == n)for(int i = 0; i <cnt; i ++)cout << ans[i]<<" ";
	else cout <<-1;
	return 0;
}

例题

我发现下面两道需要拓扑的题,只需要把一些求ans的代码放到模板里,模板还是不变的

1 .洛谷P1113 杂务

C++

/**
	每个任务前的耗时最长的任务相加即为答案,详情看洛谷深入浅出
**/
#include <iostream>
#include <vector>
#include <queue>

using namespace std;

const int N = 500005;

int ind[N], f[N], a[N];  // ind--入度   f--答案   a--时间
vector<int> edge[N];
queue<int> q;

int main() {
    int n;
    cin >> n;
    for (int i = 1; i <= n; i++) {
        int x;
        cin >> x;
        cin >> a[i];
        while (true) {
            int y;
            cin >> y;
            if (y == 0)
                break;
            edge[y].push_back(x);
            ind[x]++;
        }
    }
    // 步骤一
    for (int i = 1; i <= n; i++) {
        if (ind[i] == 0) {
            q.push(i);
            f[i] = a[i];
        }
    };
    while (!q.empty()) {
        int rhs = q.front();
        q.pop();
        // 步骤二
        for (int i = 0; i < edge[rhs].size(); i++) {
            int u = edge[rhs][i];
            ind[u]--;
            if (ind[u] == 0)
                q.push(u);  // 步骤三
            f[u] = max(f[u], f[rhs] + a[u]);
        }
    }
    int ans = 0;
    for (int i = 1; i <= n; i++) {
        ans = max(ans, f[i]);   // 统计答案
    }
    cout << ans << endl;
    return 0;
}

2.P4017 最大食物链计数

C++

/**
	先找出所有入度为0的,初始链数为1,后面搜到的每个点,都等于各个点链数之和,详情看洛谷深入浅出
**/
#include<bits/stdc++.h>
using namespace std;
const int N = 5e3 + 10;
int ans[N],outd[N],ind[N];
vector<int>G[N];
queue<int>q;
int main(){
    int n,m;
    cin >> n >> m;
    for(int i = 0; i < m; i ++){
        int x,y;
        cin >> x >> y;
        ind[y]++;
        outd[x]++;
        G[x].push_back(y);
    }
    for(int i = 1; i <= n; i ++){
        if(ind[i]==0){
            q.push(i);  
			ans[i] = 1; 
        }

}
while(q.size()){
    auto t = q.front();
    q.pop();
    for(int i = 0 ; i < G[t].size(); i ++){
        int x = G[t][i];
        ind[x] --;
		if(ind[x]==0){
            q.push(x);
        }
        ans[x] = (ans[x] + ans[t])%80112002;
    }
}
int res = 0;
for(int i = 1; i <= n; i ++){
    if(outd[i]==0){
        res= (res + ans[i])%80112002;
    }
}
cout << res;
return 0;

}

最短路径

力扣总结精华

https://leetcode.cn/circle/discuss/FyPTTM/#%E6%9C%80%E7%9F%AD%E8%B7%AF%E5%BE%84

有权单源最短路

无权单源最短路

带权全源最短路

如果是无权单源,可以BFS+队列,有权就不行了

带权单源最短路
Dijkstra

视频演示

【算法】最短路径查找—Dijkstra算法_哔哩哔哩_bilibili

各个算法用途比较

AcWing 849. Dijkstra求最短路 I - AcWing 代码详解

朴素版

C++

#include<iostream>
#include<algorithm>
#include<cstring>
using namespace std;

const int N=510;

int g[N][N];    //为稠密阵所以用邻接矩阵存储
int dist[N];    //用于记录每一个点距离第一个点的距离
bool st[N];     //用于记录该点的最短距离是否已经确定

int n,m;

int Dijkstra()
{
    memset(dist, 0x3f,sizeof dist);     //初始化距离  0x3f代表无限大

dist[1]=0;  //第一个点到自身的距离为0

for(int i=0;i<n;i++)      //有n个点所以要进行n次 迭代
{
    int t=-1;       //t存储当前访问的点

    for(int j=1;j<=n;j++)   //这里的j代表的是从1号点开始
        if(!st[j]&&(t==-1||dist[j]<dist[t]))     
            t=j;

    st[t]=true;   

    for(int j=1;j<=n;j++)           //依次更新每个点所到相邻的点路径值
        dist[j]=min(dist[j],dist[t]+g[t][j]);
}

if(dist[n]==0x3f3f3f3f) return -1;  //如果第n个点路径为无穷大即不存在最低路径
return dist[n];

}
int main()
{
    cin>>n>>m;

memset(g,0x3f,sizeof g);    //初始化图 因为是求最短路径
                            //所以每个点初始为无限大

while(m--)
{
    int x,y,z;
    cin>>x>>y>>z;
    g[x][y]=min(g[x][y],z);     //如果发生重边的情况则保留最短的一条边
}

cout<<Dijkstra()<<endl;
return 0;

}

对于「稠密图」,应当使用「朴素版」,对于「稀疏图」,应当使用「优先队列版」

堆优化版

(vector邻接表,但是大多用的还是数组模拟邻接表)

C++

/**
	邻接表存的话不用考虑重边,dij算法会取最小的
**/
#include<bits/stdc++.h>
using namespace std;
typedef pair<int,int>PII;
const int N = 1e5 + 10;
// 稀疏图用邻接表来存
vector<vector<PII>>G;
int dist[N];
bool st[N];
int n,m;
int dijkstra(){
	memset(dist,0x3f,sizeof dist);
	dist[1] = 0;
	// 这里heap中为什么要存pair呢,首先小根堆是根据距离来排的,所以有一个变量要是距离,
    // 其次在从堆中拿出来的时候要知道知道这个点是哪个点,不然怎么更新邻接点呢?所以第二个变量要存点。
    // 这个顺序不能倒,pair排序时是先根据first,再根据second,
     // 这里显然要根据距离排序,对pair排序默认先对first,所以first放距离更省事 
    priority_queue<PII,vector<PII>,greater<PII>>heap;
	heap.push({0,1});
	while(heap.size()){
		auto t = heap.top();
		heap.pop();
		int distance = t.first,node = t.second;
		if(st[node])continue;
		st[node] = true;
	
		for(int i = 0; i < G[node].size(); i ++){
			int newNode = G[node][i].first;
			int len = G[node][i].second;
			if(dist[newNode]>dist[node] + len){
				dist[newNode] = dist[node] + len;
				heap.push({dist[newNode],newNode});
			}
		}
	}
	if(dist[n] == 0x3f3f3f3f)return -1;
	else return dist[n];
}
int main(){
	cin >> n >> m;
	G.resize(n + 1);
	for(int i = 0; i < m; i ++){
		int x,y,z;
		cin >> x >> y >> z;
		G[x].push_back({y,z});
	}
	cout << dijkstra();
	return 0;
}
最短路径 和 各种第二标量

例题PTA甲级 紧急事件

C++

#include<bits/stdc++.h>
using namespace std;
int G[510][510];
int dist[510];
int num[510];//救援队
int n,m,c1,c2;
bool vis[510];
int weight[510];
int num2[510];//最短路数量
void dijkstra(){
    memset(dist,0x3f,sizeof dist);
    dist[c1] = 0;
    num2[c1] = 1;
    num[c1] = weight[c1];
    for(int i = 0; i < n ; i ++){
        int t = -1;
        for(int j = 0; j < n; j ++){
            if(vis[j]==false&&(t==-1||dist[t] >dist[j])){
                t = j;
            }
        }
        vis[t] = true;
        for(int j = 0; j < n; j ++){
            // dist[j] = min(dist[j],dist[t] + G[t][j]);
            if(dist[t] + G[t][j] < dist[j]){
                dist[j] = dist[t] + G[t][j];
                num[j] = num[t] + weight[j];
                num2[j] = num2[t];
                

        }else if(dist[t] +G[t][j] == dist[j]){
            num2[j] += num2[t];
            if(weight[j] + num[t] > num[j])
            num[j] = num[t] + weight[j];
                
                

        }
    }
}
cout << num2[c2] <<" "<<num[c2];

}
int main(){
    cin >>n >> m >>c1>>c2;
    for(int i = 0; i < n; i ++){
        cin >> weight[i];
    }
    memset(G,0x3f,sizeof G);
    while(m --){
        int x,y,z;
        cin >> x >> y >> z;
        G[x][y] = min(G[x][y],z);
        G[y][x] = min(G[y][x],z);
        // G[x][y] = G[y][x]= z;
    }
    dijkstra();
    return 0;
}

书P375

打印路径

C++

void DFS(int s,int v){ //s是起点编号,v是当前访问的顶点编号(从终点开始递归)
	if(v == s){
        printf("%d\n",s);
        return ;
    }
    DFS(s,pre[v]);
    printf("%d\n",v);
}

为什么无法处理负边

https://www.acwing.com/solution/content/6320/详解很好

bellman-ford

与spfa比可能唯一的好处:如果是有边数限制的话,就不能用spfa了

时间复杂度 N*M

如果有负权回路的话,最短路不一定存在了

bellman-ford是可以求出是否有负环的,但平时不用,时间复杂度太高,后面SPFA会用到

鸽巢(抽屉)原理

Spfa
多源汇最短路
Floyd

4.第四章 数学知识(12.10)

进制转换

(要很熟练)

转换成10进制用的秦九韶算法,迭代方式提升效率

位运算

洛谷 P1469 找筷子

CPP

#include<bits/stdc++.h>
using namespace std;
//交换律 结合律 把偶数的先结合成0,最后只剩落单的和0异或还是它自己
int ans,n,a;//ans是所有数异或之后的结果,也就是题目所求的落单的筷子的数目

int main()
{
	cin>>n;
	for (int i=1;i<=n;i++)
	{
   	  	scanf("%d",&a);
		ans^=a;//把所有的数都异或起来
	}
	printf("%d",ans);
	return 0;
}

加法原理与乘法原理

结果可能很大,要对指定数字取余数。不能全部乘完之后再取余数,因为中间结果可能会溢出。可以乘完一次就取一次余数。实际上,加法或者乘法都可以这样做:

(a+b+c)%k = ((a+b)%k + c)%k

(a * b * c) % k = ((ab)%k * c)%k

组合数问题

例题

洛谷 P2822 [NOIP2016 提高组] 组合数问题

| 同余定理,前面是除数,后面是被除数

杨辉三角的性质

C++

#include<bits/stdc++.h>
using namespace std;
long long c[2010][2010];
int main(){
	int t,k,m,n;
	cin >> t >> k;
	//先设置一个杨辉三角
    for(int i = 0; i <= 2000; i ++){
		c[i][0] = c[i][i] = 1;
		for(int j= 1; j < i ; j ++)
            //加法计算可以在途中取余
			c[i][j] = (c[i-1][j] + c[i-1][j-1])%k;
	}
    
		while(t--){
			int ans = 0;
			cin >> n >> m;
			for(int i = 0; i <= n; i ++)
				for(int j = 0; j <= min(i,m); j ++)
					ans+=c[i][j] == 0;
		
			cout << ans << endl;
		}	
		return 0;
}

约数

例题 P2926 [USACO08DEC] Patting Heads S

未解决

试除法求约数
约数个数和约数之和(两个公式)
约数个数证明:

质数和合数

所以,在以后的算法学习中,大家可以有意识地去使用线性筛进行解题,防止白给。除了这两种筛素数的方法,其实还有更加优秀的,时间复杂度低于O(N)的筛法,这里本人能力有限,就不进行介绍了,有兴趣的同学可以自己去查找相关资料。

埃氏筛

C++

void get_primes1(){
    for(int i=2;i<=n;i++){
        if(!st[i]){
            primes[cnt++]=i;
            for(int j=i;j<=n;j+=i) st[j]=true;//可以用质数就把所有的合数都筛掉;
        }
    }
}
线性筛

C++

#include <iostream>
#include <algorithm>

using namespace std;

const int N = 1000010;

//primes数组用来存放质数
int primes[N], cnt;
//st[i], i为质数则为false否则为true
bool st[N];

void get_primes(int n)
{
    for(int i = 2; i <= n; i++)
    {
        if(!st[i]) primes[cnt++] = i;
        //假设primes[0]为n最小的质因子,i为最大的因数,
        //易知若primes[i]中i>0,则会进入循环后产生多余的标记。
        for(int j = 0; primes[j] <= n / i; j ++)
        {
            //标记;primes[j]一定是primes[j]*i的最小质因子
            st[primes[j]*i] = true;
            //表明primes[j]一定是i的最小质因子,没有必要再遍历,primes要小于等于i的最小质因子
            //这样能保证每个数遍历一遍,而没有重复
            if(i % primes[j] == 0) break;
        }
    }
}

int main()
{
    int n;
    cin >> n;
    get_primes(n);
    cout << cnt << endl;
    return 0;
}

快速幂

快速幂算法 超详细教程-CSDN博客

这个博客很好

模板

活动 - AcWing

CPP

#include<bits/stdc++.h>
using namespace std;
int n;
long long int qmi(long long int base, long long int power, long long int p)
{
	long long int result = 1;   
	while (power > 0)           
	{
		if (power & 1)         							
			result = result * base % p;   
			//根据公式每个项都取余数后在再做累乘
		base = base * base % p ;   
			//根据公式每个项都取余数后在再做平方操作      						
		power >>= 1;         						
	}
			//根据公式在最后的的结果上再来一次取余数
	return result % p;       
}
int main(){
    cin >> n;
    while(n --){
        int base,power,p;
        cin >> base >> power >> p;
        cout << qmi(base,power,p)<<endl;
    }
    return 0;
}

n mod 2可以写成n & 1, n/2 可以写成 n >> 1

应用

5.第五章 动态规划(待更)

重复调用的问题适合改动态规划

斐波那契数列优化

1.递归

2.记忆化

3.dp

4.使用两个变量

动态规划就是递推的子集

背包问题

01背包问题

一维 01 逆序,完全 正序

记化化搜索

二维DP

C++

#include<bits/stdc++.h>

using namespace std;

const int MAXN = 1005;
int v[MAXN];    // 体积
int w[MAXN];    // 价值 
int f[MAXN][MAXN];  // f[i][j], j体积下前i个物品的最大价值 

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

    for(int i = 1; i <= n; i++) 
        for(int j = 1; j <= m; j++)
        {
            //  当前背包容量装不进第i个物品,则价值等于前i-1个物品
            if(j < v[i]) 
                f[i][j] = f[i - 1][j];
            // 能装,需进行决策是否选择第i个物品
            else    
                f[i][j] = max(f[i - 1][j], f[i - 1][j - v[i]] + w[i]);
        }           

    cout << f[n][m] << endl;

    return 0;
}

滚动数组(优化成一维DP)版本

CPP

for(int i = 1; i <= n; i++) 
    //这里如果从0~m的话,可能会用到上层被这层覆盖的值
   //可以自己画表
    for(int j = m; j >= 0; j--)
    {
        if(j < v[i]) 
            f[i][j] = f[i - 1][j];  // 优化前
            f[j] = f[j];            // 优化后,该行自动成立,可省略。
        else    
            f[i][j] = max(f[i - 1][j], f[i - 1][j - v[i]] + w[i]);  // 优化前
            f[j] = max(f[j], f[j - v[i]] + w[i]);                   // 优化后



完全背包
二维

CPP

#include<bits/stdc++.h>
using namespace std;
const int P = 1e3 + 10;
int N,V;
int v[P],w[P];
int mm[P][P];
// int dfs(int x,int spV){
//     if(x > N || spV < 0)mm[x][spV] = 0;
//     else if(spV < v[x]) mm[x][spV]=dfs(x+1,spV);
//   return mm[x][spV];
//             else mm[x][spV] = max(dfs(x + 1,spV),dfs(x,spV-v[x])+w[x]);
  
// }
int main(){
    cin >> N >> V;
    for(int i = 1; i <= N; i ++){
        cin >> v[i] >> w[i];
    }
    // int res = dfs(1,V);
    for(int i = 1; i <= N; i ++){
        for(int j = 1; j <= V; j ++){
            if(j < v[i])mm[i][j] = mm[i - 1][j];
    else mm[i][j] = max(mm[i - 1][j],mm[i][j - v[i]]+ w[i]);
        }
    }
    cout << mm[N][V];
    return 0;
}

//二维正序枚举,下面的写法不舒服,可以把上面的改成正序的
#include<iostream>
using namespace std;
const int N = 1010;
int f[N][N];
int v[N],w[N];
int main()
{
    int n,m;
    cin>>n>>m;
    for(int i = 1 ; i <= n ;i ++)
    {
        cin>>v[i]>>w[i];
    }

    for(int i = 1 ; i<=n ;i++)
    for(int j = 0 ; j<=m ;j++)
    {
        for(int k = 0 ; k*v[i]<=j ; k++)
            f[i][j] = max(f[i][j],f[i-1][j-k*v[i]]+k*w[i]);
    }

    cout<<f[n][m]<<endl;
}

一维

可以画图,体积从正序枚举是对的,每个物品可以无限取,从倒序枚举是错的,不然和01(只能取一次)一样了,画个图就ok

CPP

#include<iostream>
using namespace std;
const int N = 1010;
int f[N];
int v[N],w[N];
int main()
{
    int n,m;
    cin>>n>>m;
    for(int i = 1 ; i <= n ;i ++)
    {
        cin>>v[i]>>w[i];
    }

for(int i = 1 ; i<=n ;i++)
    //j小于v[i]的不改变
for(int j = v[i] ; j<=m ;j++)
{
        f[j] = max(f[j],f[j-v[i]]+w[i]);
}
	cout<<f[m]<<endl;

}

多重背包

多重背包问题 I (数据范围100,n^3没超1e7-1e8)

4. 多重背包问题 I - AcWing题库

C++

#include <iostream>
#include <algorithm>

using namespace std;
const int N = 110;

int v[N], w[N], s[N];
int f[N][N];
int n, m;

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

for(int i = 1; i <= n; i ++){//枚举背包
    for(int j = 1; j <= m; j ++){//枚举体积
        //k从0开始,可能一个都不选
        for(int k = 0; k <= s[i]; k ++){
            if(j >=  k * v[i]){
                f[i][j] = max(f[i][j], f[i - 1][j - k * v[i]] + k * w[i]);
            }
        }
    }
}

cout << f[n][m] << endl;

return 0;

}

多重背包问题 II (正常做会超)

5. 多重背包问题 II - AcWing题库

分组背包

二维

C++

#include<bits/stdc++.h>
using namespace std;

const int N=110;
int f[N][N];  //只从前i组物品中选,当前体积小于等于j的最大值
int v[N][N],w[N][N],s[N];   //v为体积,w为价值,s代表第i组物品的个数
int n,m,k;

int main(){
    cin>>n>>m;
    for(int i=1;i<=n;i++){
        cin>>s[i];
        for(int j=0;j<s[i];j++){
            cin>>v[i][j]>>w[i][j];  //读入
        }
    }

for(int i=1;i<=n;i++){
    for(int j=0;j<=m;j++){
        f[i][j]=f[i-1][j];  //不选
        for(int k=0;k<s[i];k++){
            if(j>=v[i][k])     f[i][j]=max(f[i][j],f[i-1][j-v[i][k]]+w[i][k]);  
        }
    }
}
cout<<f[n][m]<<endl;
}
一维

C++

因为只用到了第i-1列,所以可以仿照01背包的套路逆向枚举体积
#include<bits/stdc++.h>
using namespace std;

const int N=110;
int f[N];
int v[N][N],w[N][N],s[N];
int n,m,k;

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

for(int i=0;i<n;i++){
    for(int j=m;j>=0;j--){
        for(int k=0;k<s[i];k++){    //for(int k=s[i];k>=1;k--)也可以
            if(j>=v[i][k])     f[j]=max(f[j],f[j-v[i][k]]+w[i][k]);  
        }
    }
}
cout<<f[m]<<endl;

}
二维费用背包

例题 洛谷 1130红牌

例题 8. 二维费用的背包问题 - AcWing题库

背包问题的变种

01背包求方案数

例题 acwing278. 数字组合

CPP

#include<bits/stdc++.h>
using namespace std;
const int N = 1e4 + 10;
int n,m;
int a[N];
int f[N][N];
/*int dfs(int u,int spSum){
	if(f[u][spSum]!=0)return f[u][spSum];
	else if(spSum != 0 && u > n)f[u][spSum] = 0;
	else if(spSum < 0)f[u][spSum] = 0;
	else if(spSum == 0)f[u][spSum] = 1;
	else f[u][spSum] = dfs(u +1,spSum) + dfs(u + 1,spSum - a[u]);
	return f[u][spSum];
}*/

int main(){

	cin >> n >>m;
	for(int i = 1; i <= n; i ++){
		cin >> a[i];
	}
    
	for (int i = 0; i <= n+1; i++) {
        f[i][0] = 1;  // 当和为0时,方案数为1
    }
    for (int i = n; i >= 1; i--) {
        for (int j = 1; j <= m; j++) {
            f[i][j] = f[i + 1][j];  // 不选择第i个数
            if (j >= a[i]) {
                f[i][j] += f[i + 1][j - a[i]];  // 选择第i个数
            }
        }
    }
    cout << f[1][m];
	return 0;
}
01背包求具体方案

为什么逆序枚举?

12. 背包问题求具体方案 - AcWing题库

https://blog.csdn.net/yl_puyu/article/details/109960323解释很好

CPP

#include <iostream>
#include <algorithm>

using namespace std;

const int N = 1005;

int n, m;
int v[N], w[N];
int f[N][N]; 

int main() {
    cin >> n >> m;
    
    for (int i = 1; i <= n; ++i) cin >> v[i] >> w[i];
    
    for (int i = n; i >= 1; --i) 
        for (int j = 0; j <= m; ++j) {
            f[i][j] = f[i + 1][j];
            if (j >= v[i]) f[i][j] = max(f[i][j], f[i + 1][j - v[i]] + w[i]);
        }
    
    // 在此,f[1][m]就是最大数量
    int j = m;
    for (int i = 1; i <= n; ++i) 
        if (j >= v[i] && f[i][j] == f[i + 1][j - v[i]] + w[i]) {
            cout << i << ' ';
            j -= v[i];
        }
    return 0;
}

01背包求最优方案数

背包例题

云剪贴板 - 洛谷

洛谷P1802 5倍经验日(01背包变形问题)

C++

这是一道变了形的01背包

首先我们因为和每个人打都一定有经验所以一定都要打一遍。

所以不难想到max=lose[1]+lose[2]......+lose[n]+某些磕了药打赢的多出的经验值

因此我们可以进行一个转换,把价值记为win[i]-lose[i],溶剂就是要打赢磕的药,然后要使价值总和最大,然后就变成了基础的零一背包了。。。

#include<iostream>
#include<cstring>
#include<string>
#include<cmath>
#include<algorithm>
#include<cstdio>
using namespace std;
int a[100005];
long long f[1000005];
int win[100005];
int v[100005];
int lose[100005];
int main()
{
    int n,m;
    int sum=0;
    scanf("%d%d",&n,&m);
    for (int i=1;i<=n;i++){
        scanf("%d%d%d",&lose[i],&win[i],&v[i]);
        a[i]=win[i]-lose[i];
        sum=sum+lose[i];
    }
    for (int i=1;i<=n;i++){
        for (int j=m;j>=v[i];j--){
            f[j]=max(f[j],f[j-v[i]]+a[i]);
        }
    }
    printf("%lld",5*(f[m]+sum));
    return 0;
}

卡特兰数

https://leetcode.cn/circle/discuss/lWYCzv/

例题1 进出栈序列

线性DP经典例题(待更)

图上动态规划(待更)

  • 19
    点赞
  • 23
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Silver Vortex

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值