// 找是 x 的第一个。
int find(int x){
int l = 0, r = n - 1;
while (l < r){
int mid = l + r >> 1;
if (q[mid] >= x) r = mid; // 不加 = 就是大于 x 的第一个。
else l = mid + 1;
}
return l; // 不重要,l == r
}
// 找是 x 的最后一个。
int find(int x){
int l = 0, r = n - 1;
while (l < r){
int mid = l + r + 1>> 1; // 上取整
if (q[mid] <= x) l = mid; // 不加 = 就是小于 x 的最后一个。
else r = mid - 1;
}
return l;
}
在n个元素间的(n-1)个空中插入 k 个板,可以把n个元素分成k+1组的方法。
应用隔板法必须满足 3 个条件:
(1) 这n个元素必须互不相异;
(2) 所分成的每一组至少分得1个元素;
(3) 分成的组别彼此相异
标准案例:把10个相同的小球放入3个不同的箱子,每个箱子至少一个,问有几种情况?
应用:
普通隔板法:
求方程 x+y+z=10的正整数解的个数。
x、y、z不为零,每空至多插一块隔板
(n-1,m-1)=C(9,2)=36(个)
添元素隔板法:
选板法
分类插板
逐步隔板法
补充思维题:
素数筛
时间复杂度 O(n)
int prime(int n){
for (int i = 2; i < n; i++)
if (n % i == 0) return 0;
return 1;
}
时间复杂度 O(sqrt n)
int prime(int n){
int len = sqrt(n);
for (int i = 2; i < len; i++)
if (n % i == 0) return 0;
return 1;
}
如果要判断 1 - n 中的所有素数,上述方法 O(n * sqrt n)
埃拉托斯特尼筛法,埃氏筛:O(n * loglog n) 基本可以认为是 O(n)
原理:找最小的 2,删除 2 的倍数。再找次最小 3,同理。一直重复。
const int N = 1e7 + 5;
int isPrime[N], prime[N], primeNum; // 用来做筛选的数组,保存素数的数组,素数个数
void getPrime(int n){
primeNum = 0;
for (int i = 1; i <= N; i++)
isPrime[i] = 1, prime[N] = 0;
for (int i = 2; i <= N; i++){
if (isPrime[i]){
for (int j = i*2; j <= N; j+=i)
isPrime[i] = 0;
prime[++primeNum] = i;
}
}
}
欧拉筛,优化埃氏筛中重复筛选的过程。比如, 6 会被 2 和 3 同时筛选。
米勒罗宾素数检测法,一种随机检测算法,判断一个大数是否是素数。
原理:通过将底数两两合一的方法降低运算次数。
比如 13 可以转化为 1101,可以拆解为 a^13 = a^8 + a^5 = a^1
typedef long long LL;
int qmi(int a, int b){
int res = 1;
while( b ){
if (b & 1) res = (LL)res * a % MOD;
a = (LL)a * a % MOD;
b >>= 1;
}
return res;
}
按位右移: >> 除2
为了防止乘法爆int,乘法也可以重写。
int mulit(int a, int b, int mod){
int ans = 0;
while (b){
if (b & 1) ans = (ans + a) % mod;
b >>= 1;
a = (a<<1) % mod;
}
return ans;
}
排
P ( n , r ) = n ( n − 1 ) . . . ( n − r + 1 ) P ( n , r ) = n ! ( n − r ) ! P ( n , n ) = n ! 0 ! = n ! P(n, r) = n (n - 1) ... (n - r + 1) \\ P(n, r) = \frac{n!} {(n-r)!} \\ P(n, n) = \frac{n!}{0!} = n! P(n,r)=n(n−1)...(n−r+1)P(n,r)=(n−r)!n!P(n,n)=0!n!=n!
上述为线性排列,或线排列。 特别地,取出r个元素按照某种次序(如逆时针)排成一个圆圈,称这样的排列为圆排列,或循环排列。
P ( n , r ) r = n ! r ∗ ( n − r ) ! 若 r = n ( n − 1 ) ! \frac {P(n, r)} r = \frac{n!}{r * (n - r)!} \\ 若 r = n \quad (n - 1)! rP(n,r)=r∗(n−r)!n!若r=n(n−1)!
组合
C ( n , r ) = n ! r ! × ( n − r ) ! C(n, r) = \frac {n!} {r! × (n - r)!} C(n,r)=r!×(n−r)!n!
计算:
C ( n , r ) % m o d C(n, r) \% mod C(n,r)%mod
在四则运算中,加法,减法,乘法都是满足的,但是除法不行。
(a + b) % p = (a % p + b % p) % p
(a - b) % p = (a % p - b % p + p) % p // +p防止减法后的结果为负数
(a * b) % p = (a % p * b % p) % p
(a / b) % p != (a % p / b % p) % p
逆元:b是c的逆元,(a / b) % p = (a * c) % p
同余:a 和 b 对 m 取模的结果相等, a 三横 b (mod m)
推导:(a / b) % p = (a * c) % p
首先要是的 b * c 和 1 对 p 同余。即:b * c % p = 1 % p, b * c 三横 1 (mod p)
(a / b) % p = (a / b) * 1 % p = (a / b) * b * c % p = (a * c) % p
费马小定理: a 和 p 互质,且p为质数。则有 a^(p-1) 三横 1 (mod p)。
推导: a^(p-1) 三横 1 (mod p) = a*a^(p-1) 三横 1 (mod p) = a 的逆元为 a^(p-2)
(a / b) % p = (a * b^(p-2) ) % p
写法1:nlogn 的复杂度
long long Comb(int a, int b, int mod){
if (b > a) return 0;
long long ret = 1;
for (int i = 2; i <= n; i++) ret = ret * i % mod;
for (int i = 2; i <= k; i++) ret = ret * qmi(i, mod-2) % mod;
for (int i = 2; i <= n - k; i++) ret = ret * qmi(i, mod-2) % mod;
return ret;
}
写法2:
const int N = 10050;
int f[N], g[N];
f[0] = g[0] = 1;
for (int i = 1; i < N; i++){
f[i] = (LL)f[i - 1] * i % MOD;
g[i] = qmi(f[i], MOD - 2);
}
void getComb(int a, int b){
return (LL)f[a] * g[b] % MOD * g[a-b] % MOD;
}
卢卡斯定理: 主要解决当 n,m 比较大的时候,而 p 比较小的时候 <1e6
判断是不是回文串
f(i, j): 三种情况
如果 i = j, True
如果 i + 1 = j,两个数相等,s_i = s_j,则 True。
如果 大于2,则 s_i = s_j,且 f(i + 1, j - 1)
int n = s.size();
vector> f(n, vector(n));
for (int i = n - 1; i >= 0; i --)
for (int j = i; j < n; j ++){
if (i == j) f[i][j] = true;
else if (i + 1 == j) f[i][j] = s[i] == s[j];
else f[i][j] = s[i] == s[j] && f[i + 1][j - 1];
}
manacher(马拉车)算法
朴素算法 O(n^2)
vector d1(n), d2(n); // d1为奇回文的半径,奇回文半径包括中心点
for (int i = 0; i < n; i++){
d1[i] = 1; // 奇回文,没有越界,且中心点两边的数值相等,i-d1 和 i+d1
while (0 <= i - d1[i] && i + d1[i] < n
&& s[i - d1[i]] == s[i + d1[i]]){
d1[i] ++;
}
d2[i] = 0; // 偶回文,注意中起始点为中轴右边第一个,所以要判断 i-d2-1 和 i+d2
while (0 <= i - d2[i] - 1 && i + d2[i] < n
&& s[i - d2[i] - 1] == s[i + d2[i]]){
d2[i] ++;
}
}
简单优化,由于偶数中间两个一定相等,可以先做偏移,之后再从中间往两边扩散。
string longestPalindrome(string s){
int n = s.size();
int begin = 0, maxlen = 1;
int mid = 0;
while (mid < n){
int left = mid, right = mid;
while (right < n && s[right] == s[right + 1]) right ++; // 偶回文偏移
mid = right + 1; // 跳过重复点
while (left > 0 && right < n && s[left - 1] == s[right + 1])
left --, right ++;
if (right - left +1 > maxlen) {
maxlen = right - left+1;
begin = left;
}
}
return s.substr(begin, maxlen);
}
动态规划
状态:d[i][j] 表示字串 s[i..j] 是否回文
状态转换方程:d[i][j] = (s[i]==s[j]) and dp[i+1][j-1]
可以理解为:左右两边相等,且去掉左右两边后的子串是否回文。
边界条件:j-1-(i+1)+1 < 2,即 j-i+1 < 4,s[i..j] 长度为2或3时,不检查子串回文
合并:d[i][j] = (s[i]==s[j]) and (j-i<3 or dp[i+1][j-1])
初始化:单个字符一定回文,对角线 dp[i][i] = true
string longestPalindrome(string s) {
int n = s.size();
int maxlen = 1, begin = 0;
vector> dp(n, vector(n));
for (int i = n-1; i >= 0; i--)
for (int j = i; j < n; j++){
if (s[i] != s[j]) dp[i][j] = false;
else if (j - i < 3) dp[i][j] = true;
else dp[i][j] = dp[i + 1][j - 1];
if (dp[i][j] && j - i + 1 > maxlen){
maxlen = j - i + 1;
begin = i;
}
}
return s.substr(begin, maxlen);
}
马拉车算法:把字符串的 n 个字符中插入 n-1 个‘#’。
新串长度为 2*n-1,一定是奇数,且回文长度一定为 回文半径-1
#include
#include
#include
using namespace std;
const int N = 2e7 + 10;
int n;
char a[N], b[N];
int p[N];
void init(){
int k = 0;
b[k++] = '$', b[k++] = '#';
for (int i = 0; i < n; i++) b[k++] = a[i], b[k++] = '#';
b[k++] = '^';
n = k;
}
void manacher(){
int mr = 0, mid;
for (int i = 1; i < n; i++){
if (i < mr) p[i] = min(p[mid * 2 - i], mr - i);
else p[i] = 1;
while (b[i - p[i]] == b[i + p[i]]) p[i] ++;
if (i + p[i] > mr){
mr = i+ p[i];
mid = i;
}
}
}
int main(){
scanf("%s", a);
n = strlen(a);
init();
manacher();
int res = 0;
for (int i = 0; i < n; i++) res = max(res, p[i]);
printf("%d\n", res - 1);
return 0;
}
单链表
最常用:邻接表,存储树和图,(最短路,最小生成树,最大流问题)
结构体和指针:每一个节点都要new,很慢
struct Node{
int val;
Node *next;
}
数组模拟:
e[N]:链表的val
ne[N]:链表下一个节点的下标
双链表,优化某些问题
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ixdznqoz-1614603840539)(https://i.loli.net/2021/02/27/QIFKRYlMOhJPCun.png)]
快速 存储和查找 字符串集合
问题:
1、将两个集合合并
2、询问两个元素是否在一个集合中
思想:
用树维护集合。树根的编号就是整个集合的编号。
每个节点存储它的父节点,p[x]表示x的父节点。
解决方法:
1、判断树根: if (p[x] == x)
2、求x的集合编号: while(p[x] != x) x = p[x]; // 解决问题2
3、合并两个集合: px是x的集合编号,py是y的集合编号。p[x] = y; // 解决问题1
优化:
1、路径压缩:在x找根节点过程中,最后所有中间节点都会被直接指向根节点。
此后并查集可以看出O(1)复杂度
2、按秩合并,效果不明显
int gcd(int a, int b){
return b ? gcd(b, a % b) : a;
}
int gcd(int a, int b){
if (b) gcd(b, a % b);
else return a;
}
stl中的堆
1、求集合中的最小值
2、插入最小值
3、删除最小值
补充实现:stl的堆无法直接做到
4、删除任意元素
5、修改任意元素
堆的一些性质:
完全二叉树
小根堆:每一个节点都小于等于子节点
存储:
根节点从1开始,左儿子:2*x , 右儿子: 2*x
输入一个长度为n的整数数列,从小到大输出前m小的数。
输入样例:
10 5
40 2 33 26 35 8 8 26 29 2
输出样例:
2 2 8 8 26
#include
using namespace std;
const int N = 100010;
int n, m;
int q[N], len;
void down(int x){
int t = x;
if (2 * x <= len && q[2 * x] < q[t]) t = 2 * x;
if (2 * x + 1 <= len && q[2 * x + 1] < q[t]) t = 2 * x + 1;
if (t != x) {
swap(q[t], q[x]);
down(t);
}
}
void up(int x){
while ( x / 2 && h[x / 2] > h[x]){
swap(h[x / 2], h[x]);
x /= 2;
}
}
int main(){
cin >> n >> m;
for (int i = 1; i <= n; i++) cin >> q[i];
len = n;
for (int i = n / 2; i; i--) down(i); // O(n)的初始化方法
while (m --){
cout << q[1] << ' ';
q[1] = q[len];
len --;
down(1);
}
return 0;
}
本质是一个堆。队列插入时会排序,大根堆或者小根堆。弹出时弹出最大值或最小值。
和队列基本操作相同:
//升序队列,小顶堆
priority_queue ,greater > q;
//降序队列,大顶堆
priority_queue ,less >q;
greater和less是std实现的两个仿函数,就是使一个类的使用看上去像一个函数。
其实现就是类中实现一个operator(),这个类就有了类似函数的行为,就是一个仿函数类了
定义:
priority_queue a; // 大顶堆, 等同于
priority_queue, less > a;
priority_queue, greater > c; //小顶堆
用pair做优先队列元素:
pair b(1, 2);
先比较first, 后比较second。字典序
自定义类型做优先队列元素
//方法1
struct tmp1 //运算符重载<
{
int x;
tmp1(int a) {x = a;}
bool operator<(const tmp1& a) const
{
return x < a.x; //大顶堆
}
};
tmp1 a(1); tmp1 b(2); tmp1 c(3);
priority_queue d;
d.push(b); d.push(c); d.push(a);
cout << d.top().x << endl; d.pop();// 弹出时,依次弹出3,2,1
//方法2
struct tmp2 //重写仿函数
{
bool operator() (tmp1 a, tmp1 b)
{
return a.x < b.x; //大顶堆
}
};
priority_queue, tmp2> f;
f.push(b); f.push(c); f.push(a);
cout << f.top().x << endl; f.pop();// 弹出时,依次弹出3,2,1
1、 01背包:
题目:
有 N 件物品和一个容量是 V 的背包。每件物品只能使用一次。
第 i 件物品的体积是 vi,价值是 wi。
输出最大价值
题解:
dp[i][j]
不选第i个 : dp[i-1][j]
选第i个 : dp[i-1][j- v[i]] + w[i]
dp[i][j] = max(dp[i-1][j], dp[i-1][j- v[i]] + w[i])
遍历时要倒序,从大到小。去除 i 维。
dp[j] = max(dp[j], dp[j- v[i]] + w[i])
注意:
dp时如果用到 i - 1,下标要从1开始,并初始化好。
代码:
#include
using namespace std;
const int N = 1010;
int n, m;
int v[N], w[N];
int dp[N];
int main(){
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 = m; j >= v[i]; j --)
dp[j] = max(dp[j], dp[j - v[i]] + w[i]);
cout << dp[m];
return 0;
}
2、 完全背包
题目:
同01背包,但是每种物品都有无限件可用。
题解:
结论是,在01背包的代码基础上,遍历时正序即可。原理如下
dp[i][j]
选0~k个i,k*w[i]不超过背包容量。
dp[i][j] = max(dp[i-1][j], dp[i-1][j-v]+w, ... , dp[i-1][j-kv]+kw)
利用dp[i][j-v]做错位相消,得到
dp[i][j] = max(dp[i-1][j], dp[i][j- v[i]] + w[i])
遍历时要正序,从小到大。去除 i 维。
dp[j] = max(dp[j], dp[j- v[i]] + w[i])
代码:
#include
using namespace std;
const int N = 1010;
int v[N], w[N], dp[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 = v[i]; j <= m; j++)
dp[j] = max(dp[j], dp[j - v[i]] + w[i]);
cout << dp[m];
return 0;
}
3、 多重背包
题目:
同01背包,但是每种物品都可以用s次。
题解:
同完全背包,需要再遍历一次使用物品的个数。原理如下
dp[i][j]
选0~k个i,k*w[i]不超过背包容量。
dp[i][j] = max(dp[i-1][j], dp[i-1][j-v]+w, ... , dp[i-1][j-kv]+kw)
无法错位相消,优化时可以将k二进制优化
可以理解为 多个同一物品 分裂为 二进制个数物品 的累加。
eg:有某物品有8个,可以分裂为1,2,3,2个,注意最后一个数字不能超过物品总数。
复杂度从 n * v * s 优化为 n * v * log s
注意:
数组大小不是原来的N,N的大小需要乘上log M。
代码:
#include
using namespace std;
const int N = 1010* 11 , M = 2000;
int v[N], w[N], dp[M];
int main(){
int n, m, a, b, s;
cin >> n >> m;
int cnt = 1;
while (n --){
cin >> a >> b >> s;
int k = 1;
while (s >= k){
v[cnt] = k * a;
w[cnt] = k * b;
s -= k, k *= 2, cnt ++;
}
if (s > 0){
v[cnt] = s * a;
w[cnt] = s * b;
cnt ++;
}
}
for (int i = 1; i < cnt; i ++)
for (int j = m; j >= v[i]; j --)
dp[j] = max(dp[j], dp[j - v[i]] + w[i]);
cout << dp[m];
return 0;
}
4、 分组背包
把物品分成N组,每组只能选一个。背包容量M。每组si个。
题解:
dp[i][j] 表示前i组内,背包容量j内的最大价值。
每一组内的选择:不选,选第一个,...,第s个。
与01背包:只有选与不选,分组背包有多个可以选。
01背包:dp[j] = max(dp[j], dp[j- v] + w)
分组:dp[j] = max(dp[j], dp[j-v[0]] + w[0], ..., dp[j-v[s]] + w[s])
只能三重循环,无法优化
代码:
#include
using namespace std;
const int N = 110;
int dp[N];
int a[N], b[N];
int main(){
int n, m;
cin >> n >> m;
int s;
for (int i = 0; i < n; i++){ // 组数
cin >> s; // 每组内物品个数
for (int t = 0; t < s; t ++) cin >> a[t] >> b[t];
for (int j = m; j >= 0; j--) // 背包容量
for (int t = 0; t < s; t ++) // 一组内所有物品
if (j >= a[t])
dp[j] = max(dp[j], dp[j - a[t]] + b[t]);
}
cout << dp[m] << endl;
return 0;
}
1、数字三角形:
题目:
数字三角形的层数 n。
从顶部出发,在每一结点可以选择移动至其左下方的结点或移动至其右下方的结点,
一直走到底层,要求找出一条路径,使路径上的数字的和最大。
案例:
5
7
3 8
8 1 0
2 7 4 4
4 5 2 6 5
代码:
const int N = 510, INF = 1e9;
int q[N][N], dp[N][N];
int main(){
dp[1][1] = q[1][1];
for (int i = 2; i <= n; i++)
for (int j = 1; j <= i; j++) {
dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - 1]) + q[i][j];
}
int res = -INF;
for (int i = 1; i <= n; i++) res = max(res, dp[n][i]);
cout << res;
}
2、最长上升子序列
题目:给定一个长度为N的数列,求数值严格单调递增的子序列的长度最长是多少。
案例: 3 1 2 1 8 5 6 == 4
分析: dp[i] = max(dp[i], q[i-1]可选dp[i-1]+1, ..., q[1]可选dp[1]+1);
q[i]本身为一个子序列,长度1,可以通过初始化或者q[0]位置无穷小运算得到。
代码:
for (int i = 1; i <= n; i++) cin >> q[i];
q[0] = -INF;
for (int i = 1; i <= n; i++)
for (int j = 0; j <= i; j++)
if (q[i] > q[j]) dp[i] = max(dp[j] + 1, dp[i]);
int res = 0;
for (int i = 1; i <= n; i++) res = max(res, dp[i]);
cout << res;
3、最长上升子序列——二分优化
分析:
dp[i] 存储 构成长度为i的子序列,最后一个数字最小的值。
dp[i] 必定时递增的,可以二分得到新的q[i]能抵达的位置。
代码:
int len = 0;
for (int i = 0; i < n; i++){
int l = 0, r = len;
while (l < r){
int mid = l + r + 1 >> 1;
if (dp[mid] < q[i]) l = mid;
else r = mid - 1;
}
dp[l + 1] = q[i];
len = max(l+1, len);
}
cout << len;
4、最长公共子序列
题目:
两个长度分别为N和M的字符串A和B。
求既是A的子序列又是B的子序列的字符串长度最长是多少。
案例:acbd, abedc == abd == 3
分析:
dp[i][j]:表示 A中前i 和 B中前j 的最长公共子序列。
不选i,j == dp[i-1][j-1]
都选i,j == dp[i-1][j-1] + 1 (如果 a[i] = b[j])
选i,不选j == dp[i][j-1]表示一定不选j,但是i可以选可以不选,扩大了范围,但是不影响。
同理,不选i,选j 可以用 dp[i-1][j] 代替。而且dp[i-1][j-1]也被包含了。
dp[i][j] = max(dp[i][j-1], dp[i-1][j], dp[i-1][j-1] + 1 if(a[i] = b[j]));
代码: 用到 i-1, 下标从 1 开始。
cin >> a + 1;
cin >> b + 1;
for (int i = 1; i <= n; i++){
for (int j = 1; j <= m; j++){
dp[i][j] = max(dp[i][j-1], dp[i-1][j]);
if (a[i] == b[j]) dp[i][j] = max(dp[i][j], dp[i-1][j-1] + 1);
}
}
cout << dp[n][m];
5、最短编辑距离
题目:
两个字符串A和B,现在要将A经过若干操作变为B。
操作:删除A一字符,插入字符到A,替换A为另一字符。
求最少的操作步骤数
分析:
dp[i][j]:表示 A中前i 变成 B中前j 的最少操作数。
a[i] == b[j]:dp[i][j] = dp[i-1][j-1]
a[i] != b[j]:
删除 == dp[i][j-1] + 1
插入 == dp[i-1][j] + 1
替换 == dp[i-1][j-1] + 1
尤其注意初始化,dp[0][j] = j, dp[i][0] = i;
代码:
注意,由于a 和 b 输入时下标偏移 1,所以用 strlen 时一定记得 +1;
strlen在#include 内。
int edit(char a[], char b[]){
int la = strlen(a+1), lb = strlen(b+1);
for (int i = 1; i <= la; i++) dp[i][0] = i;
for (int i = 1; i <= lb; i++) dp[0][i] = i;
for (int i = 1; i <= la; i++)
for (int j = 1; j <= lb; j++)
if (a[i] == b[j]) dp[i][j] = dp[i-1][j-1];
else {
dp[i][j] = min(dp[i-1][j]+1, dp[i][j-1]+1);
dp[i][j] = min(dp[i][j], dp[i-1][j-1]+1);
}
return dp[la][lb];
}
区间DP先遍历区间长度,后遍历左右端点。
1、石子合并
题目:
N堆石子。如 4堆石子分别为 1 3 5 2。做合并操作。
先合并1、2堆,代价为4,得到4 5 2,又合并 1,2堆,代价为9,得到9 2,再合并得到11。
总代价为4+9+11=24;
输出最小代价。
案例: 1 3 5 2 == 22
分析:
dp[i][j]:表示 i 到 j 的最小代价。
最后一步一定是 某两个大堆合并成一堆。
此时,两个大堆的分界点可以是n所有隔板。i可以是1~n-1。
dp[i][j] = min(dp[i][j], dp[i][k] + dp[k+1][j] + i~j的和) k从1到j-i+1
时间复杂度 n^3
代码:
const int N = 310;
int dp[N][N];
int n, q[N];
int main(){
cin >> n;
for (int i = 1; i <= n; i++) cin >> q[i], q[i] += q[i-1]; // 前缀和
for (int len = 2; len <= n; len ++) // 枚举区间长度
for (int i = 1; i + len - 1 <= n; i++){ // 枚举左端点
int j = i + len - 1;
dp[i][j] = 1e9; // 求最小值,初始化为最大值
for (int k = i; k < j; k++) //枚举区间内所有可能
dp[i][j] = min(dp[i][j], dp[i][k] + dp[k+1][j] + q[j] - q[i-1]);
}
cout << dp[1][n];
}
数据结构 空间 性质
DFS stack O(n) 不具最短性
BFS queue O(2^n) 最短路
分类:有向图,无向图。 无向图可以理解为两条边的有向图
存储:
邻接矩阵:g[a][b] 表示 a到b 的边,值是权重,没有权重为布尔值。
浪费空间,用于稠密矩阵,稀疏矩阵不行
邻接表:单链表,每个节点都用一个单链表存。
树的邻接表实现
// 树没有环,边一定是 n-1 条,
// 以有向图的格式存储无向图,共 2n-2 条边
int h[N], e[N * 2], ne[N * 2], idx; // 链表头指针,元素值,next指针,总链表指针
void add(int a, int b) {
e[idx] = b, ne[idx] = h[a], h[a] = idx++;
}
树的dfs模板
bool st[N]; // 状态数组 st[N], 记录是否被搜索过了
void dfs(int u) {
st[u] = true;
for (int i = h[u]; i != -1; i = ne[i]) {
int j = e[i];
if (!st[j]) {
dfs(j);
}
}
}
计算树的大小
int dfs(int u){
st[u] = true;
int sum = 1; // 当前节点也算一个
for (int i = h[u]; i != -1; i = ne[i]){ // 遍历所有子树
int j = e[i]; // 当前子树节点值
if (!st[j]){
int s = dfs(j); // 返回子树大小
sum += s;
}
}
return sum;
}
树的bfs
#include
using namespace std;
const int N = 100010;
int n, m;
int h[N], e[N], ne[N], idx;
int d[N], q[N];
int bfs(){
memset(d, -1, sizeof d);
q[0] = 1;
d[1] = 0;
int begin = 0, end = 0;
while (begin <= end){
int cur = q[begin++];
for (int i = h[cur]; i != -1; i = ne[i]){
int next = e[i];
if (d[next] == -1){
d[next] = d[cur] + 1;
q[++end] = next;
}
}
}
return d[n];
}
void add(int a, int b){
e[idx] = b, ne[idx] = h[a], h[a] = idx ++;
}
int main(){
cin >> n >> m;
memset(h, -1, sizeof h);
for (int i = 0; i < m; i++){
int a, b;
cin >> a >> b;
add(a, b);
}
cout << bfs() << endl;
return 0;
}
拓扑