算法设计与分析考试说明
一、题型
选择(20分)
填空(20分)
是非(10分)
程序阅读题(20分)
算法应用题(20分)
算法设计题(10)
二、考试内容
1.算法特征
2.算法时间复杂度、空间复杂度
3.给定一个场景,选择数据结构
4.递归
5.分治法(二分查找、合并排序、快速排序)
6.贪心法(会议安排、最小生成树、哈夫曼、最短路径问题)
7.动态规划(动态规划的基本要素、矩阵连乘、0-1背包、最长公共子序列)
8.搜索法(回溯法的框架、旅行商、图的m着色问题、n皇后问题)
以下内容纯属瞎写,仅供参考
很建议看看给出的超链接文章
1.算法特征
-
有穷性:一个算法必须执行有穷步之后结束,且每一步都可在有穷时间内完成
-
确定性:算法中每条指令必须有确切的含义,对于相同的输入只能得出相同的输出
-
可行性:算法中描述的操作都可以通过已经实现的基本运算执行有限次来实现
-
输入:一个算法有零个或多个输入,这些输入取自于某个特定的对象的集合
-
输出:一个算法有一个或多个输出,这些输出是与输入有着某种特定关系的量
2.时间复杂度、空间复杂度
算法效率的度量是通过时间复杂度和空间复杂度来描述的
-
时间复杂度
- 一个语句的频度是指该语句在算法中被重复执行的次数
- 一般情况下,嵌套的循环次数时时间复杂度指数
常见的渐进时间复杂度为常对幂指阶:
O ( 1 ) < O ( l o g 2 n ) < O ( n ) < O ( n 2 ) < O ( n 3 ) < O ( 2 n ) < O ( n ! ) < O ( n n ) O(1) < O(log_2 n) < O(n) < O(n^2) < O(n^3) < O(2^n) < O(n!) < O(n^n) O(1)<O(log2n)<O(n)<O(n2)<O(n3)<O(2n)<O(n!)<O(nn)
-
空间复杂度
-
算法的空间复杂度
S(n)定义为该算法所耗费的存储空间,它是问题规模n的函数 -
一个程序在执行时除需要存储空间来存放本身所用的指令、常数、变量和输入数据外,还需要一些对数据进行操作的工作单元和存储一些为实现计算所需信息的辅助空间
-
算法原地工作是指算法所需的辅助空间为常量,即
O(1)
-
-
计算规则
- 加法规则:
O(f(n)) + O(g(n)) = O(max(f(n), g(n))) - 乘法规则:
O(f(n)) * O(g(n)) = O(f(n)*g(n))
- 加法规则:
3.给定一个场景,选择数据结构
……
4.递归
递归(Recursion):在数学和计算机科学中是指在函数的定义中使用函数自身的方法,在计算机科学中还额外指一种通过重复将问题分解为同类的子问题而解决问题的方法。
递归的基本思想是某个函数直接或者间接地调用自身,这样原问题的求解就转换为了许多性质相同但是规模更小的子问题。求解时只需要关注如何把原问题划分成符合条件的子问题,而不需要过分关注这个子问题是如何被解决的。
递归代码最重要的两个特征:结束条件和自我调用。自我调用是在解决子问题,而结束条件定义了最简子问题的答案。
int func(传入数值) {
if (终止条件) return 最小子问题解;
return func(缩小规模);
}
写递归的技巧:明白一个函数的作用并相信它能完成这个任务,千万不要试图跳进细节
递归的缺点:
在程序执行中,递归是利用堆栈来实现的。每当进入一个函数调用,栈就会增加一层栈帧,每次函数返回,栈就会减少一层栈帧。而栈不是无限大的,当递归层数过多时,就会造成 栈溢出 的后果。
显然有时候递归处理是高效的,比如归并排序; 有时候是低效的 ,因为堆栈会消耗额外空间,而简单的递推不会消耗空间。
递归优化:
比较初级的递归实现可能递归次数太多,容易超时。这时需要对递归进行优化。
- 搜索优化
- 记忆化搜索
递归与枚举的区别:
枚举是横向地把问题划分,然后依次求解子问题;而递归是把问题逐级分解,是纵向的拆分。
5.分治法(二分查找、合并排序、快速排序)
分治算法的核心思想就是“分而治之”。
分治算法可以分三步走:分解 -> 解决 -> 合并
分解原问题为结构相同的子问题。
分解到某个容易求解的边界之后,进行递归求解。
将子问题的解合并成原问题的解。
分治法能解决的问题一般有如下特征:
-
该问题的规模缩小到一定的程度就可以容易地解决。
-
该问题可以分解为若干个规模较小的相同问题,即该问题具有最优子结构性质,利用该问题分解出的子问题的解可以合并为该问题的解。
-
该问题所分解出的各个子问题是相互独立的,即子问题之间不包含公共的子问题。
如果各子问题是不独立的,则分治法要重复地解公共的子问题,也就做了许多不必要的工作。此时虽然也可用分治法,但一般用动态规划较好。
递归与分治的区别:
递归是一种编程技巧,一种解决问题的思维方式;分治算法很大程度上是基于递归的,解决更具体问题的算法思想。
二分查找
二分查找(binary search),也称折半搜索(half-interval search)、对数搜索(logarithmic search),是用来在一个有序数组中查找某一元素的算法。
-
算法原理:
以在一个升序数组中查找一个数为例。
它每次考察数组当前部分的中间元素,如果中间元素刚好是要找的,就结束搜索过程;如果中间元素小于所查找的值,那么左侧的只会更小,不会有所查找的元素,只需到右侧查找;如果中间元素大于所查找的值同理,只需到左侧查找。
-
时间复杂度:
二分查找的最优时间复杂度为 O ( 1 ) O(1) O(1)。
二分查找的平均时间复杂度和最坏时间复杂度均为 O ( l o g 2 n ) O(log_2 n) O(log2n) 。因为在二分搜索过程中,算法每次都把查询的区间减半,所以对于一个长度为 n n n的数组,至多会进行 l o g 2 n log_2 n log2n次查找。
-
空间复杂度:
迭代版本的二分查找的空间复杂度为 O ( 1 ) O(1) O(1)。
递归(无尾调用消除)版本的二分查找的空间复杂度为 O ( l o g 2 n ) O(log_2 n) O(log2n)。
/**
* 二分查找给定数组arr中给定区间[l, r]给定值key是否存在
* 如果存在返回key在数组中的下标,否则返回-1
*/
int binary_search(int arr[], int l, int r, int key) {
int mid;
while (l <= r) {
mid = l + ((r - l) >> 1); // 防止溢出
if (arr[mid] == key) {
return mid;
}
if (arr[mid] < key) {
l = mid + 1;
} else {
r = mid - 1;
}
}
return -1; // 没有找到
}
// 递归二分
int arr[MAXN];
int binary_search_recursive(int l, int r, int key) {
if (l > r) {
return -1;
}
int mid = l + ((r - l) >> 1); //防溢出
if (arr[mid] == key) {
return mid;
}
if (arr[mid] < key) {
return binary_search_recursive(mid + 1, r, key);
} else {
return binary_search_recursive(l, mid - 1, key);
}
}
合并排序
归并排序(merge sort)是一种采用了分治思想的排序算法。
归并排序是一种稳定的排序算法。
-
算法原理:
归并排序分为三个步骤:
- 将数列划分为两部分;
- 递归地分别对两个子序列进行归并排序;
- 合并两个子序列。
不难发现,归并排序的前两步都很好实现,关键是如何合并两个子序列。注意到两个子序列在第二步中已经保证了都是有序的了,第三步中实际上是想要把两个有序的序列合并起来。
-
时间复杂度:
归并排序的最优时间复杂度、平均时间复杂度和最坏时间复杂度均为 O ( n l o g 2 n ) O(n log_2 n) O(nlog2n)。
-
空间复杂度:
归并排序的空间复杂度为 O ( n ) O(n) O(n)。
/**
* 把arr[l, r]这一区间的数排序
* tmp数组是临时存放有序的版本用的
* 关键点在于一次性创建数组 避免在每次递归调用时创建 以避免对象的无谓构造和析构
*/
void merge_sort(int arr[], int tmp[], int l, int r) {
if (l >= r) {
return ;
}
int mid = l + ((r - l) >> 1);
merge_sort(arr, tmp, l, mid);
merge_sort(arr, tmp, mid + 1, r);
int la = l, ra = mid + 1, k = l;
while (la <= mid && ra <= r) {
if (arr[la] <= arr[ra]) {
tmp[k++] = arr[la++];
} else {
tmp[k++] = arr[ra++];
}
}
while (la <= mid) {
tmp[k++] = arr[la++];
}
while (ra <= r) {
tmp[k++] = arr[ra++];
}
for (int i = l; i <= r; ++i) {
arr[i] = tmp[i];
}
}
快速排序
快速排序(Quick sort),又称分区交换排序(partition-exchange sort),简称快排,是一种被广泛运用的排序算法,采用分治的方式来将一个数组排序。
快速排序是一种不稳定的排序算法。
-
算法原理:
快速排序分为三个过程:
- 将数列划分为两部分(要求保证相对大小关系);
- 递归到两个子序列中分别进行快速排序;
- 不用合并,因为此时数列已经完全有序。
-
时间复杂度:
快速排序的最优时间复杂度和平均时间复杂度为 O ( n l o g 2 n ) O(n log_2 n) O(nlog2n),最坏时间复杂度为 O ( n 2 ) O(n^2) O(n2)。
void quick_sort(int arr[], int l, int r) {
if (l >= r) {
return ;
}
int i = l, j = r;
while (i != j) {
while (arr[j] >= arr[l] && i < j) {
--j;
}
while (arr[i] <= arr[l] && i < j) {
++i;
}
swap(arr[i], arr[j]);
}
swap(arr[l], arr[i]);
quick_sort(arr, l, i - 1);
quick_sort(arr, i + 1, r);
}
6.贪心法(会议安排、哈夫曼、最短路、最小生成树)
贪心算法(greedy algorithm),是用计算机来模拟一个“贪心”的人做出决策的过程。这个人十分贪婪,每一步行动总是按某种指标选取最优的操作。而且他目光短浅,总是只看眼前,并不考虑以后可能造成的影响。
可想而知,并不是所有的时候贪心法都能获得最优解,所以一般使用贪心法的时候,都要确保自己能证明其正确性。
-
适用范围:
贪心算法在有最优子结构的问题中尤为有效。最优子结构的意思是问题能够分解成子问题来解决,子问题的最优解能递推到最终问题的最优解。
-
证明方法:
贪心算法有两种证明方法:反证法和归纳法。一般情况下,一道题只会用到其中的一种方法来证明。
- 反证法:如果交换方案中任意两个元素/相邻的两个元素后,答案不会变得更好,那么可以推定目前的解已经是最优解了。
- 归纳法:先算得出边界情况(例如 n = 1 n = 1 n=1)的最优解 F 1 F_1 F1,然后再证明:对于每个 n n n, F n + 1 F_{n +1} Fn+1都可以由 F n F_n Fn推导出结果。
-
与动态规划的区别:
贪心算法与动态规划的不同在于它对每个子问题的解决方案都做出选择,不能回退。动态规划则会保存以前的运算结果,并根据以前的结果对当前进行选择,有回退功能。
会议安排
会议安排问题,有的题目会议时间是闭区间,有的是左闭右开,需要注意。(教材为左闭右开)
闭区间的情况,贪心策略要保证下一个会议的开始时间大于上一个会议的结束时间
左闭右开区间的情况,贪心策略满足下一个会议的开始时间大于等于上一个会议的结束时间
最优前缀码
-
二元前缀码
用0-1字符串作为代码表示字符,要求任何字符的代码都不能作为其它字符代码的前缀
最短路径问题
// dijkstra + 邻接矩阵
#include <bits/stdc++.h>
using namespace std;
const int MAXN = 1e3 + 3;
const int INF = 0x3f3f3f3f;
int n, m, u, v, w, minn;
int mat[MAXN][MAXN], dis[MAXN], vis[MAXN];
void init() {
memset(vis, 0, sizeof(vis));
memset(dis, 0x3f, sizeof(dis));
for (int i = 1; i <= n; ++i) {
for (int j = 1; j <= n; ++j) {
if (i == j) {
mat[i][j] = 0;
} else {
mat[i][j] = INF;
}
}
}
}
void addEdge() {
scanf("%d %d", &n, &m);
for (int i = 1; i <= m; ++i) {
scanf("%d %d %d", &u, &v, &w);
mat[u][v] = w;
mat[v][u] = w;
}
}
void dijkstra() {
for (int i = 1; i <= n; ++i) {
dis[i] = mat[1][i];
}
vis[1] = 1;
for (int i = 1; i <= n - 1; ++i) {
minn = INF;
for (int j = 1; j <= n; ++j) {
if (vis[j] == 0 && minn > dis[j]) {
minn = dis[j];
u = j;
}
}
vis[u] = 1;
for (int j = 1; j <= n; ++j) {
if (dis[j] > dis[u] + mat[u][j]) {
dis[j] = dis[u] + mat[u][j];
}
}
}
printf("%d\n", dis[n]);
}
int main () {
init();
addEdge();
dijkstra();
return 0;
}
// dijkstra + 链式前向星 + 堆优化
#include <bits/stdc++.h>
using namespace std;
#define pii pair<int, int>
const int MAXN = 1e3 + 3;
const int INF = 0x3f3f3f3f;
int n, m, u, v, w, cnt; //n个点m条边
int dis[MAXN], vis[MAXN], head[MAXN];
struct Edge {
int to, w, next;
}edge[MAXN];
void init() {
cnt = 0;
memset(vis, 0, sizeof(vis));
memset(head, -1, sizeof(-1));
memset(dis, 0x3f, sizeof(dis));
}
void add() {
scanf("%d %d", &n, &m);
for (int i = 1; i <= m; ++i) {
scanf("%d %d %d", &u, &v, &w);
addEdge(u, v, w);
addEdge(v, u, w);
}
}
void addEdge(int u, int v, int w) {
edge[cnt].to = v;
edge[cnt].w = w;
edge[cnt].next = head[u];
head[u]= cnt++;
}
void dijkstra() {
priority_queue<pii, vector<pii>, greater<pii> > q;
dis[1] = 0;
q.push({dis[1], 1});
while (!q.empty()) {
int now = q.top.second();
q.pop();
if (vis[now]) {
continue;
}
vis[now] = 1;
for (int i = head[now]; i != -1; i = edge[i].next) {
v = edge[i].to;
if (vis[v] == 0 && dis[v] > dis[now] + edge[i].w) {
dis[v] = dis[now] + edge[i].w;
q.push({dis[v], v});
}
}
}
printf("%d\n", dis[n]);
}
int main () {
init();
add();
dijkstra();
return 0;
}
最小生成树
// Prim + 邻接矩阵
#include <bits/stdc++.h>
using namespace std;
int n, m, res, u, v, w;
int vis[103], dis[103], mat[103][103];
void init() {
res = 0;
memset(vis, 0, sizeof(vis));
memset(dis, 0x3f, sizeof(dis));
memset(mat, 0x3f, sizeof(mat));
}
int main () {
while (cin >> n >> m) {
init();
for (int i = 1; i <= m; ++i) {
cin >> u >> v >> w;
mat[u][v] = mat[v][u] = w;
}
dis[1] = 0;
for (int i = 1; i <= n; ++i) {
int p = -1;
for (int j = 1; j <= n; ++j) {
if (!vis[j] && (p == -1 || dis[p] > dis[j])) {
p = j;
}
}
res += dis[p];
vis[p] = 1;
for (int j = 1; j <= n; ++j) {
if (!vis[j] && dis[j] > mat[p][j]) {
dis[j] = mat[p][j];
}
}
}
cout << res << endl;
}
return 0;
}
/*
6 9
1 2 3
1 3 4
2 3 2
3 4 7
2 4 5
3 5 6
4 5 7
4 6 4
5 6 2
*/
// Prim + 优先队列优化
struct Edge {
int to, w;
Edge () { }
Edge (int t, int c): to(t), w(c) { }
bool operator < (const Edge &a) const {
return a.w < w;
}
}
int vis[MAXN];
vector<Edge> G[MAXN];
priority_queue<Edge> que;
void init() {
memset(vis, 0, sizeof(vis));
for (int i = 0; i <= m; ++i) {
G[i].clear();
}
while (que.size()) {
que.pop();
}
}
int prim() {
int res = 0;
vis[1] = 1;
for (int i = 0; i < G[1].size(); ++i) {
que.push(G[1][i]);
}
while (que.size()) {
Edge e = que.top();
que.pop();
if (vis[e.to]) {
continue;
}
vis[e.to] = 1;
res += e.w;
for (int i = 0; i < G[e.to].size(); ++i) {
que.push(G[e.to][i]);
}
}
return res;
}
// Kruskal算法
#include <bits/stdc++.h>
using namespace std;
int n, m, res, f[103];
struct Edge {
int u, v, w;
}e[103];
bool cmp(Edge a, Edge b){
return a.w < b.w;
}
int find(int x) {
if (x == f[x])
return x;
return f[x] = find(f[x]);
}
int main () {
cin >> n >> m;
for (int i = 1; i <= n; ++i) {
f[i] = i;
}
for (int i = 1; i <= m; ++i) {
cin >> e[i].u >> e[i].v >> e[i].w;
}
sort(e + 1, e + m + 1, cmp);
int cnt = 0;
for (int i = 1; i <= m; ++i) {
int fx = find(e[i].u);
int fy = find(e[i].v);
if (tx != ty) {
f[fx] = f[fy];
res += e[i].w;
cnt++;
}
if (cnt == n - 1) {
break;
}
}
cout << res << endl;
return 0;
}
/*
6 9
1 2 3
1 3 4
2 3 2
3 4 7
2 4 5
3 5 6
4 5 7
4 6 4
5 6 2
*/
7.动态规划(动态规划的基本要素、矩阵连乘、0-1背包、最长公共子序列)
动态规划(Dynamic programming,简称 DP)是一种在数学、管理科学、计算机科学、经济学和生物信息学中使用的,通过把原问题分解为相对简单的子问题的方式求解复杂问题的方法。
动态规划常常适用于有重叠子问题和最优子结构性质的问题,动态规划方法所耗时间往往远少于朴素解法。
动态规划背后的基本思想非常简单。大致上,若要解一个给定问题,我们需要解其不同部分(即子问题),再根据子问题的解以得出原问题的解。
通常许多子问题非常相似,为此动态规划法试图仅仅解决每个子问题一次,从而减少计算量:一旦某个给定子问题的解已经算出,则将其记忆化存储,以便下次需要同一个子问题解之时直接查表。这种做法在重复子问题的数目关于输入的规模呈指数增长时特别有用。
-
动态规划应用于子问题重叠的情况:
- 要去刻画最优解的结构特征;
- 尝试递归地定义最优解的值(就是我们常说的考虑从 i − 1 i - 1 i−1转移到 i i i);
- 计算最优解;
- 利用计算出的信息构造一个最优解。
-
动态规划的两种实现方法:
- 带备忘的自顶向下法(记忆化搜索);
- 自底向上法(将子问题按规模排序,类似于递推)。
-
动态规划原理
两个要素:
-
最优子结构
具有最优子结构也可能是适合用贪心的方法求解。
注意要确保我们考察了最优解中用到的所有子问题。
- 证明问题最优解的第一个组成部分是做出一个选择;
- 对于一个给定问题,在其可能的第一步选择中,你界定已经知道哪种选择才会得到最优解。你现在并不关心这种选择具体是如何得到的,只是假定已经知道了这种选择;
- 给定可获得的最优解的选择后,确定这次选择会产生哪些子问题,以及如何最好地刻画子问题空间;
- 证明作为构成原问题最优解的组成部分,每个子问题的解就是它本身的最优解。方法是反证法,考虑加入某个子问题的解不是其自身的最优解,那么就可以从原问题的解中用该子问题的最优解替换掉当前的非最优解,从而得到原问题的一个更优的解,从而与原问题最优解的假设矛盾。
要保持子问题空间尽量简单,只在必要时扩展。
最优子结构的不同体现在两个方面:
- 原问题的最优解中涉及多少个子问题;
- 确定最优解使用哪些子问题时,需要考察多少种选择。
子问题图中每个定点对应一个子问题,而需要考察的选择对应关联至子问题顶点的边。
-
子问题重叠
子问题空间要足够小,即问题的递归算法会反复地求解相同的子问题,而不是一直生成新的子问题。
-
自底向上的求解方法
-
矩阵链乘法
给出 n n n个矩阵的序列,希望计算他们的乘积,问最少需要多少次乘法运算?
(认为 p ∗ q p*q p∗q的矩阵与 q ∗ r q*r q∗r的矩阵相乘代价是 p ∗ q ∗ r p*q*r p∗q∗r。)
完全括号化方案是指要给出谁先和谁乘。
矩阵链乘括号化方案数:
{
p
(
n
)
=
1
n
=
1
p
(
n
)
=
∑
p
(
k
)
p
(
n
−
k
)
n
>
1
;
k
=
1
,
2
,
.
.
.
,
n
−
1
\begin{cases} p(n)=1 & n=1 \\ p(n)=\sum p(k)p(n-k) & n>1;k=1,2,...,n-1 \end{cases}
{p(n)=1p(n)=∑p(k)p(n−k)n=1n>1;k=1,2,...,n−1
状态转移方程:
m
i
,
j
=
{
0
i
=
j
m
i
n
(
m
i
,
k
+
m
k
+
1
,
j
+
p
i
−
1
p
k
p
j
)
i
≤
k
<
j
m_{i, j} = \begin{cases} 0 & i=j \\ min(m_{i,k}+m_{k+1,j}+p_{i-1}p_{k}p_j) & i \le k < j \end{cases}
mi,j={0min(mi,k+mk+1,j+pi−1pkpj)i=ji≤k<j
01背包
例题中已知条件有第 i i i个物品的重量 w i w_i wi,价值 v i v_i vi,以及背包的总容量 W W W。
设 DP 状态 f i , j f_{i,j} fi,j为在只能放前 i i i个物品的情况下,容量为 j j j的背包所能达到的最大总价值。
考虑转移。假设当前已经处理好了前 i − 1 i - 1 i−1个物品的所有状态,那么对于第 i i i个物品,当其不放入背包时,背包的剩余容量不变,背包中物品的总价值也不变,故这种情况的最大价值为 f i − 1 , j f_{i-1,j} fi−1,j;当其放入背包时,背包的剩余容量会减小 w i w_i wi,背包中物品的总价值会增大 v i v_i vi,故这种情况的最大价值为 f i − 1 , j − w i + v i f_{i-1, j - w_i} + v_i fi−1,j−wi+vi。
由此可以得出状态转移方程:
f
i
,
j
=
m
a
x
(
f
i
−
1
,
j
,
f
i
−
1
,
j
−
w
i
+
v
i
)
f_{i,j}=max(f_{i-1,j}, f_{i-1, j-w_i}+v_i)
fi,j=max(fi−1,j,fi−1,j−wi+vi)
这里如果直接采用二维数组对状态进行记录,会出现内存超限(MLE)。可以考虑改用滚动数组的形式来优化。
由于对
f
i
f_i
fi有影响的只有
f
i
−
1
f_{i-1}
fi−1,可以去掉第一维,直接用
f
i
f_i
fi来表示处理到当前物品时背包容量为
i
i
i的最大价值,得出以下方程:
f
j
=
m
a
x
(
f
j
,
f
j
−
w
i
+
v
i
)
f_j = max(f_j, f_{j - w_i} + v_i)
fj=max(fj,fj−wi+vi)
务必牢记并理解这个转移方程,因为大部分背包问题的转移方程都是在此基础上推导出来的。
/**
* n件物品,背包容量W
* 第i件物品重量w[i]价值v[i]
*/
//无优化
for (int i = 1; i <= n; ++i)
for (int j = 0; j <= W; ++j)
if (j < w[i])
dp[i][j] = dp[i - 1][j];
else
dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - w[i]] + v[i]);
//一维数组优化
for (int i = 1; i <= n; ++i)
for (int j = W; j >= w[i]; --j)
dp[j] = max(dp[j], dp[j - w[i]] + v[i]);
//更进一步的常数优化
for (int i = 1; i <= n; ++i)
{
sumw += w[i];
t = max(W - sumw, w[i]);
for (int j = W; j >= t; --j)
dp[j] = max(dp[j], dp[j - w[i]] + v[i]);
}
//不要求把背包装满,只希望价格尽量大,初始化dp[0...V]全为0
//恰好装满背包,初始化除了dp[0]为0其它dp[1...V]均初始化为-∞
最长公共子序列
子序列允许不连续。
每个 c [ i ] [ j ] c[i][j] c[i][j]只依赖于 c [ i − 1 ] [ j ] c[i-1][j] c[i−1][j]、 c [ i ] [ j − 1 ] c[i][j-1] c[i][j−1] 和 c [ i − 1 ] [ j − 1 ] c[i-1][j-1] c[i−1][j−1]。
记录最优方案的时候可以不需要额外建表(优化空间),因为重新选择一遍(转移过程)也是 O ( 1 ) O(1) O(1)的。
- 转移方程
c i , j = { 0 i = 0 , j = 0 c i − 1 , j − 1 + 1 i , j > 0 ; x i = x j m a x ( c i − 1 , j , c i , j − 1 ) i , j > 0 ; x i ≠ x j c_{i, j} = \begin{cases} 0 & i=0,j=0 \\c_{i-1,j-1} + 1 & i,j>0; x_i=x_j \\max(c_{i-1, j}, c_{i,j-1}) & i,j>0; x_i \neq x_j \end{cases} ci,j=⎩⎪⎨⎪⎧0ci−1,j−1+1max(ci−1,j,ci,j−1)i=0,j=0i,j>0;xi=xji,j>0;xi=xj
#include <bits/stdc++.h>
using namespace std;
const int MAXN = 1005;
int n, p1[MAXN], p2[MAXN], dp[MAXN][MAXN];
int main() {
scanf("%d", &n);
for (int i = 1; i <= n; ++i) {
scanf("%d", &p1[i]);
}
for (int i = 1; i <= n; ++i) {
scanf("%d", &p2[i]);
}
for (int i = 1; i <= n; ++i) {
for (int j = 1; j <= n; ++j) {
if (p1[i] == p2[j]) {
dp[i][j] = dp[i - 1][j - 1] + 1;
} else {
dp[i][j] = max(dp[i - 1][j], dp[i][j - 1]);
}
}
}
printf("%d\n", dp[n][n]);
/* 使用一维dp
for (int i = 1; i <= n; ++i) {
for (int j = 1; j <= n; ++j) {
if (p1[i] == p2[j]) {
dp[j] = dp[j - 1] + 1;
} else {
dp[j] = max(dp[j], dp[j - 1]);
}
}
}
printf("%d\n", dp[n]);
*/
return 0;
}
8.搜索法(回溯法的框架、旅行商、图的m着色问题、n皇后问题)
搜索,也就是对状态空间进行枚举,通过穷尽所有的可能来找到最优解,或者统计合法解的个数。
搜索有很多优化方式,如减小状态空间,更改搜索顺序,剪枝等。
回溯法
回溯法是一种经常被用在深度深度优先搜索(DFS)和广度优先搜索(BFS)的技巧。
其本质是:走不通就回头。
- 实现过程
- 构造空间树。
- 进行遍历。
- 如遇到边界条件,即不再向下搜索,转而搜索另一条链。
- 达到目标条件,输出结果。
回朔算法的例子:n后问题,0-1背包问题,货郎问题
- 解:向量
- 搜索空间:树,可能是n叉树、子集树、排列树等等,树的结点对应于部分向量,可行解在叶结点
- n叉树:当所给问题的n个元素中的每一个元素均有m种选择,寻找满足某种特性的n个元素取值的一种组合
- n皇后、图的m着色
- 子集树:从n个元素组成的集合S中找出满足某种性质的一个子集,相应的解空间树为子集树
- 01背包
- 排列树:从n个元素的排列中找出满足某种性质的一个排列时,相应的解空间树叫排列树
- 货郎问题、批处理作业调度
- n叉树:当所给问题的n个元素中的每一个元素均有m种选择,寻找满足某种特性的n个元素取值的一种组合
- 搜索方法:深度优先、广度优先、跳跃式遍历搜索树
// 排列树代码
def Backtrack (t):
if (t>n):
output(x)
else:
for i in range(t,n+1):
x[t], x[i]←x[i], x[t]
if (constraint(t) and bound(t)):
Backtrack(t+1)
x[t], x[i]←x[i], x[t]
// 图的m着色问题
void Backtrack((int t)//搜索函数
{
if(t>n)
{
sum++;
printf("第%d种方案:\n",sum);
for(int i=1;i<=n;i++)
cout<<x[i]<<“ ”;
cout<<endl;
}
else
for(int i=1;i<=m;i++)
{
x[t]=i;
if(OK(t)) //能着x[t]号色
Backtrack((t+1);//递归,进入更深一层继续搜索。
}
}
// 回溯法求解0-1背包问题
int i,j,n,W;
double w[M],v[M]
bool x[M],cw,cp,bestp,bestx[M];
double Bound(int i)
{
int rp=0,
while(i<=n)
{
if(x[i]==false)
rp+=v[i]
}
return rp;
}
void Backtrack(int i)
{
if(t> n)
{
for(int j=1;j<=n;j++)
bestx[j]=x[j];
bestp= cp;
return;
}
if(cw+w[t]<=c)
{
x[t] = 1;
cw+= w[t];
cp+= p[t];
Backtrack(t + 1);
cw-= w[t];
cp-= p[t];
}
if(Bound(t+ 1)> bestp)
{ x[t]=0; Backtrack(t+ 1); }
}
// 动态规划算法求解矩阵连乘问题
MatrixChain()和Traceack()
void MatrixChain(int *p,int n,int **m,int ** s)
{
for(int i=1;i<=n;i++)
{
m[i][1]=0;
s[i][1]=0;
}
for (int r=2; r<=n; r++)
for(int i=1; i<= n-r+1; i++)
{
int j= i+r-1;
m[i][j]=m[i+1][j]+ p[i-1]*p[i]*p[j];
s[i][j]= i;
for (int k=i+1; k<j; k++)
{
int t=m[i][k] +m[k+ 1][j]+p[i-1]*p[k]*p[j];
if (t<m[i][j])
{
m[i][j]= t;
s[i][j]=k;
}
}
}
}
void Traceback(int i,int j,int **s)
{
if(i== j) return;
Traceback(i, s[i][j],s);
Traceback(s[i][j]+1,j,s);
cout<<"A"<<"["<<i<<":"<<s[i][j]<<"]"<<"乘以"<<"A""["<<(s[i][j]+1)<<":"<<j<<"]"<<endl;
}
分支限界法
- 约束函数:判断能否得到可行解的隐约束
- 限界函数:判断是否有可能得到最优解的隐约束
01背包的限界条件: c p + r p > b e s t p cp + rp > bestp cp+rp>bestp
旅行商
假设有一个旅行商人要拜访N个城市,他必须选择所要走的路径,路径的限制是每个城市只能拜访一次,而且最后要回到原来出发的城市。路径的选择目标是要求得的路径路程为所有路径之中的最小值。TSP问题是一个NPC问题。
图的m着色问题
图的m-着色判定问题——给定无向连通图G和m种不同的颜色。用这些颜色为图G的各顶点着色,每个顶点着一种颜色,是否有一种着色法使G中任意相邻的2个顶点着不同颜色?
图的m-着色优化问题——若一个图最少需要m种颜色才能使图中任意相邻的2个顶点着不同颜色,则称这个数m为该图的色数。求一个图的最小色数m的问题称为m-着色优化问题。
n皇后
N皇后问题是一个经典的问题,在一个N*N的棋盘上放置N个皇后,每行一个并使其不能互相攻击(同一行、同一列、同一斜线上的皇后都会自动攻击)。
// DFS实现
#include <bits/stdc++.h>
using namespace std;
int ans[14], check[3][28] = {0}, sum = 0, n;
void eq(int line) {
if (line > n) {
sum++;
if (sum > 3)
return;
else {
for (int i = 1; i <= n; i++) printf("%d ", ans[i]);
printf("\n");
return;
}
}
for (int i = 1; i <= n; i++) {
if ((!check[0][i]) && (!check[1][line + i]) && (!check[2][line - i + n])) {
ans[line] = i;
check[0][i] = 1;
check[1][line + i] = 1;
check[2][line - i + n] = 1;
eq(line + 1);
check[0][i] = 0;
check[1][line + i] = 0;
check[2][line - i + n] = 0;
}
}
}
int main() {
scanf("%d", &n);
eq(1);
printf("%d", sum);
return 0;
}
本文详细介绍算法设计与分析的基础知识,包括算法特征、时间与空间复杂度的计算、递归、分治法、贪心法、动态规划及搜索法等内容。
943

被折叠的 条评论
为什么被折叠?



