二分
// 找是 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;
}
1、 隔板法
在n个元素间的(n-1)个空中插入 k 个板,可以把n个元素分成k+1组的方法。
-
应用隔板法必须满足 3 个条件:
(1) 这n个元素必须互不相异;
(2) 所分成的每一组至少分得1个元素;
(3) 分成的组别彼此相异 -
标准案例:把10个相同的小球放入3个不同的箱子,每个箱子至少一个,问有几种情况?
- C(n-1,m-1)=C(9, 2)
应用:
普通隔板法:
-
求方程 x+y+z=10的正整数解的个数。
-
x、y、z不为零,每空至多插一块隔板
-
(n-1,m-1)=C(9,2)=36(个)
添元素隔板法:
- 求方程 x+y+z=10的非负整数解的个数。
- x、y、z可以为零
- 给x、y、z各添加一个球,将原问题转化为求 x+y+z=13的正整数解的个数
- C(n+m-1,m-1)=C(12,2)=66(个)。
- 小结:
- 如果不能为0的问题,公式为:C(小球数 - 1, 盒子数-1)
- 如果是可以为0的问题,公式为:C(小球数+ 盒子数-1, 盒子数-1)
- 把10个相同小球放入3个不同箱子,第一个箱子至少1个,第二个箱子至少3个,第三个箱子可以放空球,有几种情况?
- 第一个箱子先放一个,第二个箱子先放3个,剩6个。
- 问题转换为 将 6 个小球放入三个箱子,可以为 0 。
- C(n+m-4-1,m-1)=C(8,2)= 28(个)。
- 将20个相同的小球放入编号分别为1,2,3,4的四个盒子中,要求每个盒子中的球数不少于它的编号数,求放法总数。(减少球数用隔板法)
- 同上
- C(n+m-1-2-3-4-1,m-1)=C(13,3)= 286(个)。
- 有一类自然数,从第三个数字开始,每个数字都恰好是它前面两个数字之和,直至不能再写为止,如257,1459等等,这类数共有几个?
- 前2位数字唯一对应符合要求的一个数,且 a+b<=9 ,且a不为0
- 转换为,将9个小球,放入三个箱子,第一个箱子不为0;
- 将一个小球放入 第一个箱子。
- C(n-1 + m -1, m-1)=C(10,2)= 45(个)。
选板法
- 有10粒糖,如果每天至少吃一粒(多不限),吃完为止,求有多少种不同吃法?
- 10颗糖,每天至少一颗。所以有 9 个空。
- 每一个空可以选择 加 或者 不加。
- 一共 2^9 = 512 种。
分类插板
- 小梅有15块糖,如果每天至少吃3块,吃完为止,那么共有多少种不同的吃法?
- 由于没有 板 的数量,所以可以枚举所有情况。
- 15颗糖,每天至少3颗,板的数量最小为1, 最大为5.
- 1天,1种
- 2天,每天先吃两颗糖,C(n - 2*2 - 1, m - 1) = C(10,1) = 10 种
- 3天,每天先吃两颗糖,C(n - 2*3 - 1, 2- 1) = C(8,2) = 28 种
- 4天,每天先吃两颗糖,C(n - 2*4 - 1, 2- 1) = C(6,3) = 20 种
- 5天,每天吃三颗糖,1 种
- 总共: 1 + 10 + 28 + 20 + 1 = 60 种
逐步隔板法
- 在一张节目单中原有6个节目,若保持这些节目相对次序不变,再添加3个节目,共有几种情况?
- 第一个节目先插入,有 6+1 个空位。
- 因此,第二个节目有 7+1 个空位,第三个节目由 8+1 个空位。
- 一共:C(7, 1)× C(8, 1) × C(9 ,1) = 504 种
补充思维题:
- 给一个集合,一共 n 个元素,从中选取 m 个元素,选出的元素中没有相邻的元素的选法一共有多少种?
- 先从 n 个小球中取 m 个小球, 剩下 n-m 个小球。有 n-m+1 个空位。
- 在 n-m+1 个空位中,选择 m 个插入取出的小球。
- 一共: C(n-m+1, m)
- 有n个不同的盒子,在每个盒子中放一些球(可以不放),使得总球数≤m,求方案数(mod p)
- 转换为:m个球,分到 n+1 个盒子里,最后一个盒子装不放的小球,所以是 n 个隔板。可以为0。
- 一共: C(m + n+1 -1, n) = C(m+n,n);(mod p)
2、素数
素数筛
时间复杂度 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 同时筛选。
米勒罗宾素数检测法,一种随机检测算法,判断一个大数是否是素数。
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;
}
4、排列组合
排
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
5、回文串问题
判断是不是回文串
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<vector<bool>> f(n, vector<bool>(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<int> 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<vector<bool>> dp(n, vector<bool>(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 <iostream>
#include <cstring>
#include <algorithm>
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)]
6、Trie树、字典树
快速 存储和查找 字符串集合
7、并查集
问题:
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、按秩合并,效果不明显
8、最大公约数
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;
}
9、堆
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 <iostream>
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;
}
优先队列
本质是一个堆。队列插入时会排序,大根堆或者小根堆。弹出时弹出最大值或最小值。
和队列基本操作相同:
- top 访问队头元素
- empty 队列是否为空
- size 返回队列内元素个数
- push 插入元素到队尾 (并排序)
- emplace 原地构造一个元素并插入队列
- pop 弹出队头元素
- swap 交换内容
//升序队列,小顶堆
priority_queue <int,vector<int>,greater<int> > q;
//降序队列,大顶堆
priority_queue <int,vector<int>,less<int> >q;
greater和less是std实现的两个仿函数,就是使一个类的使用看上去像一个函数。
其实现就是类中实现一个operator(),这个类就有了类似函数的行为,就是一个仿函数类了
定义:
priority_queue<int> a; // 大顶堆, 等同于
priority_queue<int, vector<int>, less<int> > a;
priority_queue<int, vector<int>, greater<int> > c; //小顶堆
用pair做优先队列元素:
pair<int, int> 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<tmp1> 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<tmp1, vector<tmp1>, tmp2> f;
f.push(b); f.push(c); f.push(a);
cout << f.top().x << endl; f.pop();// 弹出时,依次弹出3,2,1
动态规划DP
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 <iostream>
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 <iostream>
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 <iostream>
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 <iostream>
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;
}
2、线性DP
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 <string.h>内。
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];
}
3、区间DP
区间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<bits/stdc++.h>
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;
}
拓扑