文章目录
0. 算法基础
1. 判定问题与优化问题1:
Decision problem | Optimization problem
- 优化问题:
在计算机科学中, 优化问题是在可行解中找到最优解
优化问题分类 | 说明 |
---|---|
连续优化问题 | 诸如连续函数的优化 |
组合优化问题 | 对于离散变量的优化 |
NP
和NP-Hard
问题2:
-
NP问题: NP → \rightarrow → Non-deterministic, 在非确定性图灵机上可以被多项式时间内求解的决策问题. — 即多项式时间内可以验证解是否正确.
-
NP-Hard问题: 如果一个判定问题是NP问题, 且所有NP问题都可以约化(reduce)到它. 注意NP-Hard问题不一定是NP问题.(NP-Hard比NPC问题更广)
越想越头疼, 先跳过…
1. 状态空间
状态(state)
状态转移(state transition)
状态空间(state space)
状态变量(state variable)
价值函数(cost function)
斐波那契数列:
F 1 = F 2 = 1 F n = F n − 1 + F n − 2 ( n > 2 ) F_1 = F_2 = 1 \\ F_n = F_{n-1}+F_{n-2}(n > 2) F1=F2=1Fn=Fn−1+Fn−2(n>2)
复杂度为 O ( n ) O(n) O(n)
如果使用矩阵快速幂:
( F n F n − 1 ) = ( 1 1 1 0 ) ( F n − 1 F n − 2 ) = ( 1 1 1 0 ) ( n − 1 ) ( F 1 F 0 ) \begin{pmatrix} F_n \\ F_{n-1} \end{pmatrix} = \begin{pmatrix} 1 & 1 \\ 1 & 0 \\ \end{pmatrix} \begin{pmatrix} F_{n-1} \\ F_{n-2} \end{pmatrix} = \begin{pmatrix} 1 & 1 \\ 1 & 0 \\ \end{pmatrix}^{(n-1)} \begin{pmatrix} F_1 \\ F_0 \end{pmatrix} (FnFn−1)=(1110)(Fn−1Fn−2)=(1110)(n−1)(F1F0)
矩阵 ( 1 1 1 0 ) \begin{pmatrix} 1 & 1 \\ 1 & 0 \end{pmatrix} (1110) 的n-1次幂使用快速幂: T ( n ) = T ( n 2 ) + O ( 1 ) T(n)=T(\frac{n}{2})+O(1) T(n)=T(2n)+O(1), 因此 T ( n ) = O ( l o g 2 n ) T(n)=O(log_2n) T(n)=O(log2n)
拨转开关问题:
有一个4$\times$4的灯阵, 每个灯上均有一个开关, 每次拨动开关会使当前灯和相邻灯的开关状态改变, 判断给出某个开关图案是否可以通过操作开关使全部灯都打开?
4$\times$4灯阵的每一种开关状态都对应一个状态, 如果用一个二维矩阵来存储一个状态, 空间开销是 2 16 × 16 2^{16}\times 16 216×16, 对于一个 4 × 4 4\times 4 4×4的灯阵来说似乎还可以接受, 但是空间复杂度是指数增长的.
- 状态压缩: 使用16个二进制位来表示
- 状态哈希: H : X → Y H: X \rightarrow Y H:X→Y
埃及分数问题:
古埃及数学的分数表示十分特殊, 不允许分子不为1的分数存在, 比如 2 3 \frac{2}{3} 32在古埃及数学中只能表示为 1 2 + 1 6 \frac{1}{2} + \frac{1}{6} 21+61, 请设计算法, 对给定的真分数$\frac{a}{b} $, 请计算出满足以下条件的埃及分数表示:
- 和式中分数互不相同
- 和式中分数个数最少
- 满足条件2的情况下, 保留和式中最小分数的最大情况下的解 .
例如: 19 45 = 1 5 + 1 6 + 1 18 \frac{19}{45}=\frac{1}{5} + \frac{1}{6} + \frac{1}{18} 4519=51+61+181
定义状态: a ′ b ′ = a b − 1 x \frac{a'}{b'}=\frac{a}{b}-\frac{1}{x} b′a′=ba−x1
{ 广 度 优 先 ? : x ∈ [ 1 , + ∞ ] 深 度 优 先 ? : x 可 以 无 穷 大 , 可 以 永 远 递 归 下 去 ! 现 在 还 是 优 化 问 题 ! → 每 一 步 都 没 有 界 , 导 致 无 法 找 到 一 个 判 定 准 则 . \begin{cases} 广度优先?: x\in [1, +\infin] \\ 深度优先?: x可以无穷大, 可以永远递归下去! 现在还是优化问题!\rightarrow 每一步都没有界, 导致无法找到一个判定准则. \end{cases} {
广度优先?:x∈[1,+∞]深度优先?:x可以无穷大,可以永远递归下去!现在还是优化问题!→每一步都没有界,导致无法找到一个判定准则.
- 迭代加深搜索(Iteratively Deepening DFS): 搜索深度也作为状态的一部分, 优化问题转化为判定问题.
- 必须要注意, 这个深度是迭代深度
这里还待优化, 有三个TLE
#include<iostream>
#include<vector>
#include<utility>
using namespace std;
vector<int> print_vec;
inline bool operator<(vector<int> a, vector<int> b) {
if (a.size() != b.size()) {
return a.size() > b.size();
} else {
return a[a.size() - 1] > b[b.size() - 1];
}
}
inline bool operator>(vector<int> a, vector<int> b) {
return !(move(a) < move(b));
}
inline pair<int, int> sub(int numerator, int denominator, int v) {
numerator = numerator * v - denominator;
denominator = denominator * v;
return pair<int, int>(numerator, denominator);
}
inline int gcd(int a, int b) {
if (b == 0) return a;
return gcd(b, a % b);
}
inline void yuefen(int &numerator, int &denominator) {
int GCD = gcd(numerator, denominator);
numerator /= GCD;
denominator /= GCD;
}
inline int dfs(int numerator, int denominator, int last_denominator, int depth, int max_depth, vector<int> result) {
if (numerator == 0) {
return 1;
} else if (depth > max_depth){
return 0;
}
yuefen(numerator, denominator);
int bingo = 0;
for (int i = last_denominator + 1; i <= min(denominator / numerator * (max_depth - depth + 1), 10000000); i++) {
if (numerator * i - denominator >= 0) {
pair<int, int> res = sub(numerator, denominator, i);
int _numerator = res.first;
int _denominator = res.second;
result.push_back(i);
int finish = dfs(_numerator, _denominator, i, depth + 1, max_depth, result);
if (finish) {
bingo = finish;
}
if (depth == max_depth && finish) {
if (print_vec.empty() || result > print_vec) {
print_vec.swap(result);
}
break;
}
result.pop_back();
}
}
return bingo;
}
int main() {
int numerator, denominator;
cin >> numerator >> denominator;
int MAX = 1;
int last = 1;
vector<int> result;
while (!dfs(numerator, denominator, last, 1, MAX, result)) {
MAX++;
}
// for (int i = print_vec.size() - 1; i >= 0; i--) {
// cout << print_vec[i] << ' ';
// }
for(auto item : print_vec) {
cout<<item<<' ';
}
cout << endl;
return 0;
}
八数码问题:
九个格子中放入了数字1~8方块, 利用空位移动方块, 问使数字恢复顺序所需最少移动次数?
如果用一个二维数组来存储状态的转移, 则共有 9 9 9^9 99种状态… 广度优先搜索内存必然不够
- 双向广度优先搜索(Bidirectional BFS, Meet-in-the-mid):
- 初始状态和结束状态都是已知的
- 可以从初始状态和结束状态同时进行广度优先搜索
- Meet-in-the-middle: https://www.geeksforgeeks.org/meet-in-the-middle/
数字三角形:
如图所示,请找出一条自顶向下路径,使得该路径上所有数字之和最大。
-
记忆化搜索:
- 状态变量: ( i , j ) (i,j) (i,j)表示路径自上而下走到了第 i i i行第 j j j列
- 有些路径都会重复经过同一个节点
在搜索过程中, 对于已经搜索过、且价值函数的值不会再度更新(或者更优)的状态,可以使用记忆化搜索的方式,规避重复搜索。
-
动态规划算法适用问题的必要条件:
- 最优子结构: 最优解可以由子问题的最优解构造出来 – 分而治之
- 重叠子问题: 一些子问题在搜索过程中会重复遇到 – 具有优化空间
- 无后效性: 子问题最优解一旦确定就不会被更新
-
动态规划算法设计的三个关键环节:
- 定义状态和价值函数(建模)
- 确定初始状态和对应价值(边界)
- 确定状态转移中的价值更新方式(状态转移方程)
-
动态规划的两种实现:
- 记忆化搜索(Memorization): 自顶向下
- 打表法(Tabulation): 自底向上
https://www.luogu.com.cn/problem/P1216
#include<iostream>
using namespace std;
int main() {
int n;
cin >> n;
int a[n][n];
int dp[n][n];
for (int i = 0; i < n; i++) {
for (int j = 0; j < i + 1; j++) {
cin >> a[i][j];
}
}
for (int i = n - 1; i >= 0; i--) {
for (int j = 0; j < i + 1; j++) {
if (i == n - 1) {
dp[i][j] = a[i][j];
} else {
dp[i][j] = max(dp[i+1][j], dp[i+1][j+1]) + a[i][j];
}
}
}
cout<<dp[0][0]<<endl;
return 0;
}
一维递推问题: 爬楼梯(求和形式)
一个有n级台阶的楼梯,每步可以迈1、2、3级,问爬完这段楼梯有几种不同的方式?
F ( n ) = ∑ i = 1 3 F ( n − i ) F(n)=\sum_{i=1}^3 F(n-i) F(n)=i=1∑3F(n−i)
https://www.luogu.com.cn/problem/P1255
#include<iostream>
using namespace std;
int main() {
int n;
cin>>n;
unsigned long long dp[n+1];
for (int i = 0; i <= n; i++) {
if (i == 0 || i == 1) {
dp[i] = 1ll;
} else {
dp[i] = dp[i-1] + dp[i-2];
}
}
cout<<dp[n]<<endl;
return 0;
}
一维递推问题: 最长上升子序列(最值形式)
给一个数组,请找出这个数组中的一个最长的单调递增子序列(可以不连续)
F ( i ) = m a x 1 ≤ j < i { I ( a j < a i ) F ( j ) } + 1 边 界 : F ( 0 ) = 0 答 案 : F ( n ) = m a x 1 ≤ j < i { F ( i ) } , 答 案 可 以 在 构 建 时 不 断 维 护 更 新 ! F ( i ) 表 示 以 a [ i ] 为 子 序 列 结 束 字 符 的 最 长 上 升 子 序 列 长 度 , 这 一 点 非 常 重 要 F(i)=\underset{1\le j\lt i}{max} \{\mathbb{I}(a_j < a_i)F(j)\} + 1 \\ 边界:F(0)= 0 \\ 答案: F(n)=\underset{1\le j \lt i}{max}\{F(i) \}, 答案可以在构建时不断维护更新! \\ F(i)表示以a[i]为子序列结束字符的最长上升子序列长度, 这一点非常重要 F(i)=1≤j<imax{ I(aj<ai)F(j)}+1边界:F(0)=0答案:F(n)=1≤j<imax{ F(i)},答案可以在构建时不断维护更新!F(i)表示以a[i]为子序列结束字符的最长上升子序列长度,这一点非常重要
https://leetcode-cn.com/problems/longest-increasing-subsequence/
class Solution {
public:
int lengthOfLIS(vector<int>& nums) {
int len = nums.size();
int dp[len];
int res = 0;
dp[0] = 1;
for (int i = 0; i < len; i++) {
int tmp = 0;
for (int j = 0; j < i; j++) {
tmp = max(tmp, (nums[i] > nums[j] ? dp[j] : 0));
}
dp[i] = tmp + 1;
res = max(res, dp[i]);
}
return res;
}
};
二维问题: 区间问题 - 最长回文子序列
f ( i , j ) → 区 间 [ i , j ] 内 的 最 长 回 文 子 序 列 长 度 f ( i , j ) = m a x { f ( i + 1 , j ) , f ( i , j − 1 ) , f ( i + 1 , j − 1 ) + 2 I ( a i = a j ) } 答 案 : f ( 1 , n ) 边 界 : f ( i , i ) = 1 f(i,j)\rightarrow 区间[i,j]内的最长回文子序列长度 \\ f(i,j) = max\{ f(i+1, j), f(i,j-1), f(i+1, j-1) + 2\mathbb{I}(a_i=a_j) \} \\ 答案: f(1, n) \\ 边界: f(i, i) = 1 \\ f(i,j)→区间[i,j]内的最长回文子序列长度f(i,j)=max{ f(i+1,j),f(i,j−1),f(i+1,j−1)+2I(ai=aj)}答案:f(1,n)边界:f(i,i)=1
https://leetcode-cn.com/problems/longest-palindromic-subsequence/
class Solution {
public:
int longestPalindromeSubseq(string s) {
int len = s.size();
int dp[len][len];
for(int w = 0; w < len; w++)