第二章 递归
2.1递归的概念
递归思想(降维)
将一个大型复杂的问题层层转化为一个(或几个)与原问题相似的规模较小的问题来求解(递归本体)。继续下去知道子问题简单到能够直接求解(递归出口)。
1.子问题须与原问题为同样的事,且更为简单(规模更小);
2.不能无限制地调用本身,须有个出口,化简为非递归状况处理。
递归算法
一个直接或间接调用自身的算法称为递归算法。
一个使用函数自身给出定义的函数称为递归函数。
阶乘函数的定义:就是一个递归定义式。
int jie(int n) {
if (n == 1) return 1;
return jie(n - 1) * n;
}
2.2递归算法的应用和实现
如果问题的数据结构是递归的,问题的定义是递归的,问题的解法是递归的,可以考虑用递归算法。
2.2.1递归算法的应用
2.2.1.1问题的数据结构是递归的
2.2.1.1.1链表
这是单链表,每个结点有两个域:数据域和指针域。它是一种递归的结构,可定义为:1)一个结点,其指针域为null,是一个单链表;2)一个结点,其指针域为有效指针指向一个单链表,仍是一个单链表。
建立链表有几个注意事项:
1.链表的定义:
typedef struct node{
int data;
struct node* next;
}*point;
因为定义里面要用到指针,所以结构体命名的时候不可以不写名字。
后面写一个重命名:*point,方便。
2.链表的输入:(尾插法)
void input(point& h,int n) {
h = new node;
if (!h) { cout << "创建失败!" << endl; return; }
int x;
point q = h;
while (n--) {
cin >> x;
point p = new node;
p->data = x;
q->next = p;
q = p;
}
q->next = NULL;//(这里用q不用p是因为如果用p的话,在外面要new node)
}
最重的是:循环里面每循环一次就要给p一次空间,引用。
头指针也要new node(给一个空间)
在最后,写p->next=NULL:
注意函数的形参,头指针要设置为引用,要修改。
3.打印链表(两种,正常的打印(迭代),递归的打印)
递归算法,有一点问题是,因为要一直往回溯,所以要回溯到h->data,但是h->data没有初始化,所以这里要处理一下。
//输出链表
//迭代(正着输出)
void output(point h) {
point p = h->next;
while (p != NULL) {
cout << p->data << " ";
p = p->next;
}
}
//递归(逆着输出)
void out(point p){
if (p == NULL) {
return;
}
if (p->next == NULL) {
cout << p->data << " ";
return;
}
out(p->next);
if(p!=h)
cout << p->data << " ";
}
//输出会有点问题,重新设置一下,不再直接输出h,而是新设了一个
//p,让p来递归,同时可以用p和h判断是否相等。
//把上面的递归修改一下
void out(point p){
if (p == NULL) {
return;
}
if (p->next == NULL) {
cout << p->data << " ";
return;
}
out(p->next);
cout << p->data << " ";
}
//这样的话,在主函数里面调用的时候,out(h->next);
补充一个递归算法,
比上面那个简单,
//这个是正着输出
void out(point h) {
if (h == NULL)return;
if (h->next == NULL)return;
cout << h->next->data<<" ";
out(h->next);
}
**任务1:打印最后一个结点:
因为链表本身也是一种递归的结构,就是大的是h作为头指针的链表,依次往下看是,h->next作为头指针的链表,以此类推,直到链表为空,输出。
//实现打印最后一个结点(递归)
point find_last(point h) {
if (h == NULL)return NULL;//这里包含了只有头指针的情况,
//因为本来头指针就没有数据域,如果指针域也为空,就直接为空了。
if (h->next == NULL)cout << h->data << endl;
find_last(h->next);
}
//消除尾递归
point find_last2(point h) {
if (h == NULL)return NULL;
point p = h;
while (p->next != NULL) {
p = p->next;
}
cout << p->data << endl;
}
**任务2:单链表逆序:
//链表逆置
point nizhi(point p,point& h) {
if (h == NULL)return NULL;
if (p->next == NULL) {
h->next = p;
return p;
}
point temp = nizhi(p->next, h);
p->next = NULL;
temp->next = p;
return p;
}
point nizhi01(point h) {
if (h == NULL || h->next == NULL) {
return h;
}
//注意要有temp,temp一直不变
point temp = nizhi01(h->next);
h->next->next = h;
h->next = NULL;
return temp;
}
第二个方法,返回值直接作为h。但是调用的时候是h->next.
第一个直接调用函数即可,在函数内部修改了h。
第一个方法是返回值为新链表的头结点,但是要不断地将要插入新链表的结点的next域置空,而且指控之后,再回溯一次就不空了,除了第一个节点。
第二个方法是,返回值直接为新的头结点的下一个节点(首元结点),然后让h去递归,层层深入。
2.2.1.1.2二叉树
图上这个0是表示没有结点,方便区分左右结点。
先序遍历序列是1 2 3 4 5 6 10 11 7 8
代码实现:
输入:1 2 3 4 0 0 5 0 0 0 6 10 11 0 0 0 7 0 8 0 0
//定义二叉树
typedef struct bTree {
int data;
struct bTree* rchild, * lchild;
}*tpoint;
//创建二叉树(递归)
void set(tpoint& th) {
int x;
cin >> x;
if (x == 0) {
th = NULL; return;
}
th = new bTree;
th->data = x;
set(th->lchild);
set(th->rchild);
}
//二叉树的遍历
//先序(递归)
void bian_x(tpoint th) {
if (th == NULL) {
return;
}
cout << th->data << " ";
bian_x(th->lchild);
bian_x(th->rchild);
}
//用栈实现先序
stack <tpoint> s;
void bian_s(tpoint th) {
if (th == NULL)return;
s.push(th);
while (!s.empty()) {
tpoint tp = s.top();
s.pop();
//cout << "size: " << s.size() << endl;
while (tp != NULL) {
cout << tp->data << " ";
s.push(tp->rchild);
tp = tp->lchild;
}
}
}
//后序遍历(递归)
void bian_h(tpoint th) {
if (th == NULL) {
return;
}
bian_h(th->lchild);
bian_h(th->rchild);
cout << th->data << " ";
}
运行结果:
1 2 3 4 0 0 5 0 0 0 6 10 11 0 0 0 7 0 8 0 0
递归遍历:
1 2 3 4 5 6 10 11 7 8
栈遍历:
1 2 3 4 5 6 10 11 7 8
后序遍历:
4 5 3 2 11 10 8 7 6 1
任务1:求树的深度
树的深度=max{左子树的深度,右子树的深度}
//树的深度
int high_t(tpoint th,int h) {
if (th == NULL)return h;
h++;
int lh = high_t(th->lchild, h);
int rh = high_t(th->rchild, h);
return max(lh, rh);
}
//树的深度2
int h_s(tpoint th) {
if (th == NULL)return 0;
int h1 = h_s(th->lchild) + 1;
int h2 = h_s(th->rchild) + 1;
return max(h1, h2);
}
任务2:求叶子结点的个数
叶子数目=左边的叶子结点的数目+右边的叶子结点的数目。
//求叶子结点的数目
int num = 0;
void yezi(tpoint th) {
if (th == NULL)return;
if (th->lchild == NULL&&th->rchild==NULL) { num++; return; }
yezi(th->lchild);
yezi(th->rchild);
}
//叶子结点02
int yenu(tpoint th) {
if (th == NULL)return 0;
if (th->rchild == NULL && th->lchild == NULL) {
return 1;
}
int n1 = yenu(th->lchild);
int n2 = yenu(th->rchild);
return n1 + n2;
}
2.2.1.2问题的定义是递归的
这种先if( ),递归出口
然后再写递归本体
2.2.1.2.1Fibonacci数列
F i b ( n ) = { 1 n = 0 1 n = 1 F i b ( n − 1 ) + F i b ( n − 2 ) n > 1 Fib(n)=\begin{cases} 1&n=0\\ 1&n=1\\ Fib(n-1)+Fib(n-2)&n>1 \end{cases} Fib(n)=⎩⎪⎨⎪⎧11Fib(n−1)+Fib(n−2)n=0n=1n>1
递归方法和非递归方法(数组存储):
//递归
int f(int n) {
if (n == 0)return 1;
if (n == 1)return 1;
return f(n - 1) + f(n - 2);
}
//非递归
const int N = 1e5;
int a[N];
//非递归(循环)
int f2(int n) {
if (n < 0)return -1;
if (n < 2)return 1;
int x = 1, y = 1,z=0;
for (int i = 2; i <= n; i++) {
z = x + y;
x = y;
y = z;
}
return z;
}
int main() {
int t, n;
cout << "请输入要进行的次数:" << endl;
cin >> t;
cout << "非递归:" << endl;
int m;
cout << "请输入要计算n的最大值:" << endl;
cin >> m;
a[0] = 1, a[1] = 1;
for (int i = 2; i <= m; i++) {
a[i] = a[i - 1] + a[i - 2];
}
int t1 = t;
while (t--) {
cin >> n;
cout << a[n] << " ";
}
cout << endl << "递归:" << endl;
while (t1--) {
cin >> n;
cout << f(n) << " ";
}
cout << "非递归(循环):" << endl;
while (t2--) {
cin >> n;
cout << f2(n) << " ";
}
个人觉得还是我写的数组比较好。
2.2.1.3问题的解法是递归的
2.2.1.3.1整数划分问题
将一个正整数n表示成一系列正整数之和,n=n1+n2+…+nk,其中n1≥n2≥…≥nk≥ 1,k ≥1正整数n的一个这种表示称为正整数n的一个划分。正整数n的不同划分个数称为正整数n的划分数,记做p(n)。
整数划分,我们先定义一个函数q(n,m)表示用不大于m的数表示n有几种可能。先考虑几种特殊情况,m>n时,q(n,m)=q(n,n);
q(n,n)又可以变换成q(n,n-1)+1(n=m);
当m=1时,q(n,m)=1;
最常见的情况是n>m>1,这时q(n,m)可以看作是用了m,不用m分开两类q(n,m)=q(n,m-1)+q(n-m,m){前面一项时没有用,后面一项是必须用}
q
(
n
,
m
)
=
{
0
n
<
1
或
m
<
1
1
m
=
1
q
(
n
,
n
)
n
<
m
1
+
q
(
n
,
n
−
1
)
n
=
m
q
(
n
,
m
−
1
)
+
q
(
n
−
m
,
m
)
n
>
m
>
1
q(n,m)=\begin{cases} 0&n<1或m<1\\ 1&m=1\\ q(n,n)&n<m\\ 1+q(n,n-1)&n=m\\ q(n,m-1)+q(n-m,m)&n>m>1 \end{cases}
q(n,m)=⎩⎪⎪⎪⎪⎪⎪⎨⎪⎪⎪⎪⎪⎪⎧01q(n,n)1+q(n,n−1)q(n,m−1)+q(n−m,m)n<1或m<1m=1n<mn=mn>m>1
代码实现:
int q(int n, int m) {
if (n < 1 || m < 1)return 0;
if (m == 1||n==1)return 1;
if (n <= m)return q(n, n-1)+1;
return q(n, m - 1) + q(n - m, m);
}
2.2.1.3.2设计模拟汉诺塔问题求解过程的算法。
汉诺塔问题的描述是:设有3根标号为A,B,C的柱子,在A柱上放着n个盘子,每一个都比下面的略小一点,要求把A柱上的盘子全部移到C柱上,移动的规则是:(1)一次只能移动一个盘子;(2)移动过程中大盘子不能放在小盘子上面;(3)在移动过程中盘子可以放在A,B,C的任意一个柱子上。
问题分析:可以用递归方法求解n个盘子的汉诺塔问题。
基本思想:1个盘子的汉诺塔问题可直接移动。n个盘子的汉诺塔问题可递归表示为,首先把上边的n-1个盘子从A柱移到B柱,然后把最下边的一个盘子从A柱移到C柱,最后把移到B柱的n-1个盘子再移到C柱。
const int N = 1e5;
int pl[N], n;
char A = 'A', B = 'B', C = 'C';
void han(char A, char B, char C,int m) {
if (m == 1) {
cout << m << "号盘: from " << A << " to " << C << endl;
return;
}
han(A, C, B, m - 1);
cout << m << "号盘: from " << A << " to " << C << endl;
han(B, A, C, m - 1);
}
2.2.2递归算法的设计方法
递归算法既是一种有效的算法设计方法,也是一种有效的分析问题的方法。递归算法求解问题的基本思想是:对于一个较为复杂的问题,把原问题分解成若干个相对简单且类同的子问题,这样,原问题就可递推得到解。
适宜于用递归算法求解的问题的充分必要条件是:(1)问题具有某种可借用的类同自身的子问题描述的性质;(2)某一有限步的子问题(也称作本原问题)有直接的解存在。
当一个问题存在上述两个基本要素时,该问题的递归算法的设计方法是:
(1)把对原问题的求解设计成包含有对子问题求解的形式。(写成一个递归函数)
(2)设计递归出口。
设计举例:
2.2.2.1猴吃桃问题
猴子吃桃。有一群猴子摘来了一批桃子,猴王规定每天只准吃一半加一只(即第二天吃剩下的一半加一只,以此类推),第九天正好吃完,问猴子们摘来了多少桃子?
我的分析:因为第九天刚好吃完了,所以原本第九天只有两个桃子。
设一个函数f(x)(表示第x天开始的时候桃子的数目)
f
(
x
)
=
{
2
x
=
n
0
x
=
n
+
1
(
f
(
x
+
1
)
+
1
)
∗
2
x
<
n
f(x)=\begin{cases} 2&x=n\\ 0&x=n+1\\ (f(x+1)+1)*2&x<n \end{cases}
f(x)=⎩⎪⎨⎪⎧20(f(x+1)+1)∗2x=nx=n+1x<n
利用递归写算法:
递归出口时x=n或者x=n+1时。
int tao(int n,int i) {
if (i == n)return 2;
return (tao(n, i + 1) + 1) * 2;
}
int main() {
int n;
cin >> n;
cout << tao(n, 1);
上面的方法有点浪费空间,可以写成尾递归形式:
//尾递归
int tao2(int n, int s) {
return (n == 1) ? s : tao2(n - 1, 2 * (s + 1));
}
2.2.2.2输出形式
例1 设计一个输出如下形式数值的递归算法。
n n n … n
…
3 3 3
2 2
1
由题可见,递归出口是n=1,递归本体是一直输出n个n.
void shuchu(int n) {
if (n == 1) {
cout << "1" << endl;
return;
}
for (int i = 1; i <= n; i++) {
cout << n << " ";
}
cout << endl;
shuchu(n - 1);
}
2.2.2.3委员会问题
设计求解委员会问题的算法。委员会问题是:从一个有n个人的团体中抽出k (k≤n)个人组成一个委员会,计算共有多少种构成方法。
问题分析:从n个人中抽出k(k≤n)个人的问题是一个组合问题。把n个人固定位置后,从n个人中抽出k个人的问题可分解为两部分之和:第一部分是第一个人包括在k个人中,第二部分是第一个人不包括在k个人中。对于第一部分,则问题简化为从n-1个人中抽出k-1个人的问题;对于第二部分,则问题简化为从n-1个人中抽出k个人的问题。
先分析问题将问题转化为递归形式的函数:
找到递归出口。
c(n,k)=c(n-1,k)+c(n-1,k-1);
c
(
n
,
k
)
=
{
1
n
=
k
n
k
=
0
c
(
n
−
1
,
k
)
+
c
(
n
−
1
,
k
−
1
)
n
>
k
c(n,k)=\begin{cases} 1&n=k\\ n&k=0\\ c(n-1,k)+c(n-1,k-1)&n>k \end{cases}
c(n,k)=⎩⎪⎨⎪⎧1nc(n−1,k)+c(n−1,k−1)n=kk=0n>k
设计程序:
int f(int n,int k) {
if (n == k)return 1;
if (k == 1)return n;
return f(n - 1, k - 1) + f(n - 1, k);
}
2.2.2.4求两个正整数n和m最大公约数
辗转相除法:(a>b)
a/b=p,a%b=q
求a,b的最大公约数就是求b,q的最大公约数,当q=0时,b就是最大公约数。求最大公约数的公式定义为f(a,b)
问题分析:公式1:
f
(
a
,
b
)
=
{
f
(
b
,
a
)
b
>
a
f
(
b
,
a
%
b
)
a
%
b
!
=
0
b
a
%
b
=
0
f(a,b)=\begin{cases} f(b,a)&b>a\\ f(b,a\%b)&a\%b!=0\\ b&a\%b=0 \end{cases}
f(a,b)=⎩⎪⎨⎪⎧f(b,a)f(b,a%b)bb>aa%b!=0a%b=0
公式2:
f
(
a
,
b
)
=
{
f
(
b
,
a
)
b
>
a
f
(
b
,
a
%
b
)
a
%
b
!
=
0
a
b
=
0
f(a,b)=\begin{cases} f(b,a)&b>a\\ f(b,a\%b)&a\%b!=0\\ a&b=0 \end{cases}
f(a,b)=⎩⎪⎨⎪⎧f(b,a)f(b,a%b)ab>aa%b!=0b=0
程序设计:
int f(int a, int b) {
if (a < b) {
f(b, a);
}
if (b == 0)return a;
f(b, a % b);
}
2.2.3递归过程的实现
方法调用离不开栈,递归不过是一种特殊的方法调用,即所谓的“自己调用自己”。
每递归一次,增加一层,函数一样,参数不同,本地变量的值不同。
每层间的关系满足先进后出的特性,这是栈。
所以递归要通过栈这个数据结构来维护方法间的调用关系,而且要保存每一层的本地变量的值。
调用函数在调用被调用函数前,系统要保存以下两类信息:
(1)调用函数的返回地址;
(2)调用函数的局部变量值。
当执行完被调用函数,返回调用函数前,系统首先要恢复调用函数的局部变量值,然后返回调用函数的返回地址。
递归函数被调用时,系统要作的工作和非递归函数被调用时系统要作的工作在形式上类同.
递归函数被调用时,系统的运行时栈也要保存上述两类信息。每一层递归调用所需保存的信息构成运行时栈的一个工作记录,在每进入下一层递归调用时,系统就建立一个新的工作记录,并把这个工作记录进栈成为运行时栈新的栈顶;每返回一层递归调用,就退栈一个工作记录。因为栈顶的工作记录必定是当前正在运行的递归函数的工作记录,所以栈顶的工作记录也称为活动记录。
2.3递归问题的非递归算法
一般说来,递归过程的实现效率是非常低的,每次递归调用都必须首先做诸如参数替换、环境保护等事情。造成效率低下的另一个重要的原因是大量的重复计算。
例如Fibonacci数列,
F
i
b
(
n
)
=
{
1
n
=
0
1
n
=
1
F
i
b
(
n
−
1
)
+
F
i
b
(
n
−
2
)
n
>
1
Fib(n)=\begin{cases} 1&n=0\\ 1&n=1\\ Fib(n-1)+Fib(n-2)&n>1 \end{cases}
Fib(n)=⎩⎪⎨⎪⎧11Fib(n−1)+Fib(n−2)n=0n=1n>1
如果要求f(5)
在计算的过程中,会重复多次计算f(3),f(2),f(1)等等;导致效率很低
2.3.1将递归算法转化为非递归算法的方法:
2.3.1.1设计迭代算法
如果一个函数既有递归形式的定义又有非递归的迭代形式的定义,则可以用循环结构设计出迭代算法。一般说来,如果在一个函数或过程中只递归调用它一次,那么它的计算或执行过程可以看成是线性变化的。
2.3.1.1.1例如:求阶乘算法。
f ( n ) = n ! 按 递 归 形 式 写 : f ( n ) = { n ∗ f ( n − 1 ) n > 0 1 n = 0 f(n)=n!\\ 按递归形式写: f(n)=\begin{cases} n*f(n-1)&n>0\\ 1&n=0 \end{cases} f(n)=n!按递归形式写:f(n)={n∗f(n−1)1n>0n=0
在阶乘算法中,每个f(n)只被调用一次,所以可写成循环来实现;
int n, t;
//递归
int f1(int n) {
if (n < 0)return -1;
if (n == 0)return 1;
return n * f1(n - 1);
}
//非递归
int f2(int n) {
if (n < 0)return -1;
int s = 1;
for (int i = 1; i <= n; i++) {
s *= i;
}
return s;
}
//尾递归
int f3(int n,int as) {
if (n <= 1) {
return as;
}
f3(n - 1, as * n);
}
顺序执行、循环和跳转是冯·诺依曼计算机体系中程序设计语言的三大基本控制结构,这三种控制结构构成了千姿百态的算法,程序,乃至整个软件世界。递归也算是一种程序控制结构,但是普遍被认为不是基本控制结构,因为递归结构在一般情况下都可以用精心设计的循环结构替换,因此可以说,递归就是一种特殊的循环结构。
补充知识:
令f(x)表示正整数x末尾所含有的“0”的个数,则有:
当0 < n < 5时,f(n!) = 0;
当n >= 5时,f(n!) = k + f(k!), 其中 k = n / 5(取整)。
也可以写成递归形式。
2.3.1.1.2Fibnaocci数列
见前(2.2.1.2)
2.3.1.2消除尾递归:递归调用是最后一步操作
可以用循环结构通过设置一些工作单元,把递归算法转化为非递归算法。开始令工作单元等于外层的实际参数,以后随着循环的执行,不断向里层变化,直到原递归调用的最里层的情况。循环结束后,执行原属于最里层的操作,而后整个算法结束。
###插入:尾递归
递归调用是最后一步操作;
如果一个函数中所有递归形式的调用都出现在函数的末尾,我们称这个递归函数是尾递归的。当递归调用是整个函数体中最后执行的语句且它的返回值不属于表达式的一部分时,这个递归调用就是尾递归。尾递归函数的特点是在回归过程中不用做任何操作,这个特性很重要,因为大多数现代的编译器会利用这种特点自动生成优化的代码。
例子:猴吃桃:见前(2.2.2.1)
阶乘(见前:2.3.1.1.1)
尾递归的效果就是去除了将下层的结果再次返回给上层,需要上层继续计算才得出结果的弊端。
其实,寻找单链表的最后一个结点并打印其数据域的值的过程search就是一个尾递归过程。
可以将这个代码用循环实现,消除尾递归。(见前:2.2.1.1.1)
2.3.1.3利用堆栈
递归的实现是基于堆栈的,当一个递归问题不容易找到它的迭代算法又不属于尾递归时,通常通过引入一个工作栈保存“返回位置”以实现过程调用一返回控制;这一思想同样适用于递归消除。下面以先根遍历为例,具体讨论如何用工作栈消除递归。
首先必须弄清工作栈的作用方式,即弄清怎样用工作栈控制先根遍历的“走向”。二叉链表上的任一X以及它的左XL和右子树XR。假设t是指向结点X的指针.
具体代码见前(2.2.1.1.2)
通过上面的例子可以看出,工作栈在消除递归中的基本作用是提供一种控制机构。在非递归算法执行过程中的某些“关键”时刻,用栈顶元素来“引导”下一步操作的“走向”。为了达到这一目的,必须提前将这些有用的信息进栈保存。在上面的例子中,工作栈保存的是各个结点的右指针,这些指针也就是二叉树上各结点的右子树的根指针。显然,这些根指针正是先根遍历递归算法 中包含的一些递 归调用的实参;这些实参在非递归算法中若不及时保存就会丢失。因此,递归算法中的调用一返回控制被工作栈的作用所取代,从而将递归算法转换成非递归算法。
阶乘也可以,不写了。
2.4递归方程解的渐进阶的求法
定理:设a,b,c是非负常数,n是c的整幂,则递归方程:
T
(
n
)
=
{
b
若
n
=
1
a
T
(
n
/
c
)
+
b
n
若
n
>
1
的
解
是
:
T
(
n
)
=
{
O
(
n
)
若
a
<
c
O
(
n
l
o
g
2
n
)
若
a
=
c
O
(
n
l
o
g
c
a
)
若
a
>
c
T(n)= \begin{cases} b&若n=1\\ aT(n/c)+bn&若n>1 \end{cases} \\的解是:\\ T(n)=\begin{cases} O(n)&若a<c\\ O(nlog_2n)&若a=c\\ O(n^{log_ca})&若a>c \end{cases}
T(n)={baT(n/c)+bn若n=1若n>1的解是:T(n)=⎩⎪⎨⎪⎧O(n)O(nlog2n)O(nlogca)若a<c若a=c若a>c
第三章 分治法
3.1分治法的基本思想
分治法与软件设计的模块化方法非常相似。为了解决一个大的问题,可以: 1) 把它分成两个或多个更小的问题; 2) 分别解决每个小问题; 3) 把各小问题的解答组合起来,即可得到原问题的解答。小问题通常与原问题相似,可以递归地使用分而治之策略来解决。
3.1.1分治法适用条件:(本质上是一种递归)
1.该问题的规模缩小到一定程度就可以容易地解决
2.该问题可以分解为若干个规模较小的子问题,即该问题具有最有子结构性质。
3.利用该问题分解出的子问题的解可以合并为该问题的解。
4.该问题所分解出的子问题的解可以合并为该问题的解。不包含公共子问题(fibnaocci)
3.1.2分治的具体过程:
if 问题规模小到可以直接解决
直接解决该问题(递归出口)
else
将问题分解为k个规模较小的子问题
for(i=1;i<=k;i++)
递归调用该分治算法,分别解决每一个子问题。
将各子问题的解合并为原问题的解。
3.1.3一般算法设计模式如下:
Divide_and_Conquer(P)
if |P|≤n0 (递归出口或者算法规模)
then return(ADHOC(P))
将P分解为较小的子问题P1、P2、…、Pk
for i←1 to k
do
yi ← Divide_and_Conquer(Pi) △ 递归解决Pi
T ← MERGE(y1,y2,…,yk) △ 合并子问题
Return(T)
根据分治法的分割原则,原问题应该分为多少个子问题才较适宜?各个子问题的规模应该怎样才为适当?这些问题很难予以肯定的回答。但人们从大量实践中发现,在用分治法设计算法时,最好使子问题的规模大致相同。换句话说,将一个问题分成大小相等的k个子问题的处理方法是行之有效的。
3.1.4分治策略的时间复杂度分析:
T ( n ) = a f ( n / b ) + d ( n ) 他 的 时 间 复 杂 度 采 用 递 归 算 法 提 到 的 m a s t e r 定 理 T(n)=af(n/b)+d(n)\\ 他的时间复杂度采用递归算法提到的master定理 T(n)=af(n/b)+d(n)他的时间复杂度采用递归算法提到的master定理
3.1.5例题:[伪币问题]:
给你一个装有1 6个硬币的袋子。1 6个硬币中有一个是伪造的,并且那个伪造的硬币比真的硬币要轻一些。给你一台没有砝码的天平,试用较少的测量次数找出这个伪币。
问题分析:十六个硬币找到质量最小的硬币,将其分成两份
可先将其分为两组,分别找到最小值,然后两个最小值取最小值。②
然后重复②,知道问题的规模可以求解。
程序设计:
int min_a(int l, int r) {
if (l == r)return a[l];//其实不会来这里,因为只能知道两个之间的大小,但是不会知道每个的重量
return min(min_a(l, (l + r) / 2), min_a((l + r) / 2 + 1, r));
//这里可以扩充为下面的
}
int min_b(int l, int r) {
if (l == r)return a[l];//出口,问题可以解决
int m = (l + r) / 2;//问题分解
int x = min_b(l, m);//递归解决小问题
int y = min_b(m + 1, r);
return min(x, y);//用小问题的解,来求解大问题。
}
3.2典型样例
3.2.1快速排序
分析:快速排序,每次只排一个元素的位置。
i | j | ||||
---|---|---|---|---|---|
1 | 2 | 3 | 4 | 5 | 6 |
13 | 1 | 5 | 9 | 6 | 12 |
- 选定ki为起点,且将ki送至x;
- 从末项kn开始起,将指针j倒着向前找到第一个kj<x.key或i>=j时,则判i<j?是,kj送ki,i=i+1 ;
- 从ki起指针i向前扫描,找到第一个ki>x.key或i>=j时,则判i<j?是,ki送kj,j=j-1;
- 上述过程进行到i=j时停止,将x送至ki,同时i=i+1,j=j-1,即x在正确位置上,并分k为k1和k2两个子集合;
- 重复调用上述过程,直到将整个集合排序好为之;
快排的基本算法:
void qrsort(int l,int r){
if(l==r)return;
int m = partition(a,l,r);
qsort(l,m);
qsort(m+1,r);
}
现在要实现partition算法,找到要插入的位置,并且,在寻找的过程中要移位。
int partition(int l,int r) {
int x = a[l];
int i = l;
int j = r;
while (j!=i) {
while (a[j] >= x&&j>i)j--;
if (j > i)a[i] = a[j],i++;
while (a[i] <= x && j > i)i++;
if (j > i)a[j] = a[i],j--;
}
a[i] = x;
return i;
}
3.2.2折半查找
问题描述:给定已排序好的n个元素a[1:n],现在要在这n个元素中找出一特定元素x。
问题分析:将n个元素分成大致相同的两半,取a[n/2]与x做比较,如果x=a[n/2]则,则找到x,算法终止;如果x≤a[n/2],则只要在a的左半部继续搜索;如果x>a[n/2],则只要在a的右半部继续搜索。
int cha(int l, int r,int x) {
if (l > r || l<1 || r>n)return 0;
//防止找不到(可能在中间,也可能在两端找不到)
int m = (l + r) / 2;
//m来记录中间元素的位置
if (x == a[m])return m;
//如果找到了就直接return位置,没找到的话,根据要寻找的值与中间的比较再合适的区间继续寻找。
if (x > a[m])return cha(m + 1, r, x);
return cha(l, m - 1, x);
}
在最坏情况下二分查找法的复杂度为O(logn)。
3.2.3分治—归并排序
问题:n个元素的排序问题,即将n个元素按一定规则排列成一个有序序列。
基本思想:对n个元素等分,分别对小的集合排序,一次降维,当n=1时终止排序,否则将待排序元素分成大小大致相同的两个子集,分别对两个子集进行排序,最终将排序好的子集合并成为所要求的排序好的集合。
void Cope(int x[], int y[],int l,int r,int k) {
for (int i = l; i <= r; i++) {
x[k++] = y[i];
}
}//将y数组复制到x数组中,x数组是从k开始,y数组是从l->r
//二路归并算法,将a数组分为两份(两部分分别有序),第一部分l->r,第二部分r->m;
void Merge(int l, int r, int m) {
int i = l, j = r + 1, k = l;
while (i <= r && j <= m) {//如果两部分都有元素就继续循环
//将二者中小的先依次存入b数组中
if (a[i] <= a[j])b[k] = a[i], i++;
else b[k] = a[j], j++;
k++;
}
//将剩下的直接复制到b数组的后部分
if (i <= r)Cope(b, a, i, m, k);
if (j <= m)Cope(b, a, j, r, k);
Cope(a, b, l, k, l);//将排好序的b数组赋值给a数组
}
//归并排序,从中间分开,直到每部分只有一个元素
void Sort_merge(int l, int r) {
if (l == r) { b[l] = a[l]; return; }
int m = (l + r) / 2;
Sort_merge(l, m);
Sort_merge(m + 1, r);
Merge(l, m, r);
}
3.2.4金块问题
老板有一袋金块(共n块,n是2的幂(n>=2)),将有两名最优秀的雇员每人得到其中的一块,排名第一的得到最重的那块,排名第二的雇员得到袋子中最轻的金块。假设有一台比较重量的仪器,我们希望用最少的比较次数找出最重的金块。
问题分析:
这个问题可以分为两部分,求最大值和求最小值。
以最大值为例,求最大值,可以用递归,先将大问题分解为两个子问题,求大问题的最小值其实就是求两个子问题的最小值的最小值。以此类推,直到子问题可以求解,即当r=l时。
求最小值与求最大值方法一样。
将二者合并起来的话,原本单独求一个,返回值类型为int,但是现在返回值类型变为void,将max和min作为引用形参,传回。
void Max_Min(int l, int r, int& mi, int& ma) {
if (l == r) {
mi = a[l], ma = a[l];
return;
}
int m = (l + r) / 2;
int x=0, y=0, z=0, s=0;
Max_Min(l, m, x, y);
Max_Min(m + 1, r, z, s);
mi = min(x, z);
ma = max(y, s);
}
3.2.5大整数乘法(二进制数)
问题描述:设x,y都是n位的二进制整数,现在要计算它们的乘积xy;
按正常的乘法规则要做
n
2
n^2
n2
次一位数的乘法,计算步骤太多。下面用分治法来解决此问题。
直接将数据分开, 将n位的二进制整数X和Y各分为2段,每段的长为n/2位(为简单起见,假设n是2的幂),如图所示。
由
此
,
x
=
A
2
n
/
2
+
B
,
y
=
C
2
n
/
2
+
D
.
这
样
,
x
和
y
的
乘
积
为
:
x
y
=
(
A
2
n
/
2
+
B
)
(
C
2
n
/
2
+
D
)
=
A
C
2
n
+
(
A
D
+
C
B
)
2
n
/
2
+
B
D
(
1
)
由此,x=A2^{n/2}+B,y=C2^{n/2}+D.这样,x和y的乘积为: \\xy=(A2^{n/2}+B)(C2^{n/2}+D)= \\AC2^n+(AD+CB)2^{n/2}+BD\qquad(1)
由此,x=A2n/2+B,y=C2n/2+D.这样,x和y的乘积为:xy=(A2n/2+B)(C2n/2+D)=AC2n+(AD+CB)2n/2+BD(1)
上述算法的时间复杂性为:设T(n)是2个n位整数相乘所需的运算总数,则由式(1),有:
{
T
(
1
)
=
1
T
(
n
)
=
4
T
(
n
/
2
)
+
O
(
n
)
(
2
)
\begin{cases} T(1)=1\\ T(n)=4T(n/2)+O(n)\qquad(2)\\ \end{cases}
{T(1)=1T(n)=4T(n/2)+O(n)(2)
解 得 T ( n ) = O ( n 2 ) 。 由 此 可 见 , 仍 需 要 n 2 次 乘 法 。 将 ( 1 ) 该 写 为 下 面 的 形 式 : x y = A C 2 n + [ ( A − B ) ( D − C ) + A C + B D ] 2 n / 2 + B D 虽 然 式 子 看 起 来 比 式 ( 1 ) 复 杂 些 , 但 他 仅 需 要 做 3 次 n / 2 位 整 数 的 乘 法 ( A C , B D 和 ( A − B ) ( D − C ) ) , 6 次 加 、 减 法 和 2 次 移 位 。 由 此 可 得 : { T ( 1 ) = 1 T ( n ) = 3 T ( n / 2 ) + c n ( 4 ) 其 解 为 T ( n ) = O ( n l o g 3 ) = O ( n 1.59 ) 解得T(n)=O(n^2)。由此可见,仍需要n^2次乘法。\\将(1)该写为下面的形式:\\xy=AC2^n+[(A-B)(D-C)+AC+BD]2^{n/2}+BD \\虽然式子看起来比式(1)复杂些,但他仅需要做3次n/2位整数的乘法\\ (AC,BD和(A-B)(D-C)),6次加、减法和2次移位。由此可得:\\ \begin{cases} T(1)=1\\ T(n)=3T(n/2)+cn&(4) \end{cases} \\其解为T(n)=O(n^{log3})=O(n^{1.59}) 解得T(n)=O(n2)。由此可见,仍需要n2次乘法。将(1)该写为下面的形式:xy=AC2n+[(A−B)(D−C)+AC+BD]2n/2+BD虽然式子看起来比式(1)复杂些,但他仅需要做3次n/2位整数的乘法(AC,BD和(A−B)(D−C)),6次加、减法和2次移位。由此可得:{T(1)=1T(n)=3T(n/2)+cn(4)其解为T(n)=O(nlog3)=O(n1.59)
分治法优化算法:可以缩小问题规模,原本有四个合并一下,变成三个
不会写二进制数的,写了十进制数的:
代码里面的阶次要用pow而不是用^
int cheng(int num1, int num2, int n) {
if(num1==0||num2==0)return 0;
if (n == 1) {
return num1 * num2;
}
int A = num1 / pow(10,n/2);
int B = num1 - A * pow(10, n / 2);
int C = num2 / pow(10,n/2);
int D = num2 - C * pow(10, n / 2);
int m1 = cheng(A, C, n / 2);
int m2 = cheng((A - B), (D - C), n / 2);
int m3 = cheng(B, D, n / 2);
return m1 * pow(10,n) + (m1 + m2 + m3) * pow(10, n / 2) + m3;
}
3.2.6Strassen矩阵乘法
引例:正常的矩阵乘法A(n,m)B(m,k)=>C(n,k)
const int N = 1e3;
int A[N][N], B[N][N], C[N][N];
int n, m, k;
//A,B为已知的矩阵,C为待求矩阵,n为A矩阵的行数,m为A矩阵的列数和B矩阵的行数,k为C矩阵的列数
void input(int a[N][N],int n,int m) {
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= m; j++) {
cin >> a[i][j];
}
}
}
void jucheng() {
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= k; j++) {
C[i][j] = 0;
for (int s = 1; s <= m; s++) {
C[i][j] += A[i][s] * B[s][j];
}
}
}
}
void out(int a[N][N],int n, int m) {
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= m; j++) {
cout << a[i][j] << " ";
}
cout << endl;
}
}
如 果 m = n = k , 那 么 算 法 的 时 间 复 杂 度 为 O ( n 3 ) 如果m=n=k,那么算法的时间复杂度为O(n^3) 如果m=n=k,那么算法的时间复杂度为O(n3)
问题分析:分治法
若A和B是2个n×n的矩阵,则它们的乘积C=AB同样是一个n×n的矩阵。A和B的乘积矩阵C中的元素C[i,j];
首先,我们还是需要假设n是2的幂。将矩阵A,B和C中每一矩阵都分块成为4个大小相等的子矩阵,每个子矩阵都是n/2×n/2的方阵。由此可将方程C=AB重写为:
KaTeX parse error: Undefined control sequence: \matrix at position 9: \left[ \̲m̲a̲t̲r̲i̲x̲{ C_{11} & C_{1…
如果n=2,则2个2阶方阵的乘积可以计算出来,共需8次乘法和4次加法。当子矩阵的阶大于2时,为求2个子矩阵的积,可以继续将子矩阵分块,直到子矩阵的阶降为2。这样,就产生了一个分治降阶的递归算法。
因
此
上
述
的
算
法
时
间
耗
费
T
(
n
)
应
该
满
足
:
{
T
(
2
)
=
b
T
(
n
)
=
8
T
(
n
/
2
)
+
c
n
2
n
>
2
这
个
递
归
方
程
的
解
仍
然
是
T
(
n
)
=
O
(
n
3
)
.
没
有
降
低
算
法
复
杂
度
S
t
r
a
s
s
e
n
提
出
了
一
种
新
的
算
法
来
计
算
2
个
2
阶
方
阵
的
乘
积
。
他
的
算
法
只
用
了
7
次
乘
法
运
算
,
但
增
加
了
加
、
减
法
的
运
算
次
数
。
这
7
次
乘
法
是
:
M
1
=
A
11
(
B
12
−
B
22
)
M
2
=
(
A
11
+
A
12
)
B
22
M
3
=
(
A
21
+
A
22
)
B
11
M
4
=
A
22
(
B
21
−
B
11
)
M
5
=
(
A
11
+
A
22
)
(
B
11
+
B
22
)
M
6
=
(
A
12
−
A
22
)
(
B
21
+
B
22
)
M
7
=
(
A
11
−
A
21
)
(
B
11
+
B
12
)
做
完
这
7
次
乘
法
后
因此上述的算法时间耗费T(n)应该满足:\\ \begin{cases} T(2)=b\\ T(n)=8T(n/2)+cn^2&n>2 \end{cases} \\这个递归方程的解仍然是T(n)=O(n^3).没有降低算法复杂度\\ Strassen提出了一种新的算法来计算2个2阶方阵的乘积。\\他的算法只用了7次乘法运算,但增加了加、减法的运算次数。\\这7次乘法是: M_1=A_{11}(B_{12}-B_{22})\\ M_2=(A_{11}+A_{12})B_{22}\\M_3=(A_{21}+A_{22})B_{11} \\M_4=A_{22}(B_{21}-B_{11})\\ M_5=(A_{11}+A_{22})(B_{11}+B_{22}) \\M_6=(A_{12}-A_{22})(B_{21}+B_{22}) \\M_7=(A_{11}-A_{21})(B_{11}+B_{12}) \\做完这7次乘法后
因此上述的算法时间耗费T(n)应该满足:{T(2)=bT(n)=8T(n/2)+cn2n>2这个递归方程的解仍然是T(n)=O(n3).没有降低算法复杂度Strassen提出了一种新的算法来计算2个2阶方阵的乘积。他的算法只用了7次乘法运算,但增加了加、减法的运算次数。这7次乘法是:M1=A11(B12−B22)M2=(A11+A12)B22M3=(A21+A22)B11M4=A22(B21−B11)M5=(A11+A22)(B11+B22)M6=(A12−A22)(B21+B22)M7=(A11−A21)(B11+B12)做完这7次乘法后
3.2.7求数组中的第k小元素
利用分治策略,用快速排序来解决。先对数组分组,然后判断第k小的元素应该在哪个分组,然后递归该分组,最后求得第k小的元素。
不用像快速排序一样,全部排完,只需要将需要的区间排好即可。
const int N = 1e5;
int n, a[N], k;
//利用partition来寻找合适的位置
int partition(int l,int r) {
int x = a[l];
int i = l;
int j = r;
while (j!=i) {
while (a[j] >= x&&j>i)j--;
if (j > i)a[i] = a[j],i++;
while (a[i] <= x && j > i)i++;
if (j > i)a[j] = a[i],j--;
}
a[i] = x;
return i;
}
//返回的是地址,这里认定数组输入是从1开始的
int fk(int l, int r) {
if (k > r||k<1)return 0;
int m = partition(l, r);
if (m == k)return m;
if (k < m)return fk(l, m-1);
return fk(m + 1, r);
}
int main() {
cout << "请输入元素数目:" << endl;
cin >> n;
cout << "请输入元素:" << endl;
for (int i = 1; i <= n; i++) {
cin >> a[i];
}
cout << "请输入要查找的位次:" << endl;
cin >> k;
int x=fk(1, n);
if (!x)cout << "查找不到!" << endl;
else {
cout << a[x] << endl;
}
第四章 动态规划(DP)
1.基本要素:(1)最优子结构性质 (2)重叠子问题性质 (3)无后向性
无后向性:此结点的最优结果与前面的无关
最优子结构性质:大问题最优,小问题也最优
重叠:递归定义
但是用递推
分步骤,多策略,最优化
2.基本思想:分治思想,解决冗杂计算
3.求解步骤:
- 刻画最优子结构性质
- 递归定义最优解
- 自底向上求解最优值(策略选择)
- 构造最优解
4.掌握:
- 确定dp数组(dp table)以及下标的含义
- 确定递推公式
- dp数组如何初始化
- 确定遍历顺序
- 举例推导dp数组
5.求解步骤:
- 划分阶段:按照问题的时间或空间特征,把问题分为若干个阶段。注意这若干个阶段一定要是有序的或者是可排序的(即无后向性),否则问题就无法用动态规划求解。
- 选择状态:将问题发展到各个阶段时所处于的各种客观情况用不同的状态表示出来。当然,状态的选择要满足无后效性。
- 确定决策并写出状态转移方程:之所以把这两步放在一起,是因为决策和状态转移有着天然的联系,状态转移就是根据上一阶段的状态和决策来导出本阶段的状态。所以,如果我们确定了决策,状态转移方程也就写出来了。但事实上,我们常常是反过来做,根据相邻两段的各状态之间的关系来确定决策。(递归)
- 写出规划方程(包括边界条件):动态规划的基本方程是规划方程的通用形式化表达式。一般说来,只要阶段、状态、决策和状态转移确定了,这一步还是比较简单的。
6.备忘录法:
备忘录方法是动态规划法的一个变形。备忘录方法往往用一个表格或者数组把已经解决的子问题保存起来,下次解时,察看该子问题的解,而不必计算,备忘录方法采用的是由顶向下的方式,动态规划法采用的是由低向上的方式,备忘录方法的控制结构与递归算法相同。区别仅在于在递归解决之前先看看问题是否已经解决。
初始时,我们可以为每一个子问题建立一个记录项,存入一个特殊值,表示子问题尚未解决。求解过程中,看该记录项是否为特殊值,如果是,则该子问题第一次求解,解决并保存,反之,则说明已经解决过,可以直接使用。
其实就是对递归算法加了一些东西,对于重复解没有必要再次计算,直接调用就可以。
4.1最短路径问题(引言)
现有一张地图,各结点代表城市,两结点间连线代表道路,线上数字表示城市间的距离。如图所示,试找出从结点A到结点E的最短距离。
先来分析问题(递归,分治思想):设f(x)表示,x到E的最短路径,求A到E的最短路径即f(A)=min(5+f(B1),3+f(B2)).
f(b1)=min(1+f(C1),6+f(c2),3+f(C3));f(B2)=min(8+f(C2),4+f(C4));
…
f(E)=0;
如果用递归求解,会有重复计算,复杂度较高。要消除这种出重复计算。
把递归改写成递推,可以用数组来实现。
1.刻画最优解性质
我
们
用
s
[
i
]
,
i
=
1
,
2
,
3
,
表
示
一
个
点
到
E
的
最
短
路
径
用
g
[
i
]
[
j
]
,
表
示
图
,
各
个
结
点
之
间
的
联
系
我们用s[i],i=1,2,3,表示一个点到E的最短路径\\ 用g[i][j],表示图,各个结点之间的联系
我们用s[i],i=1,2,3,表示一个点到E的最短路径用g[i][j],表示图,各个结点之间的联系
2.递归定义最优值
s
[
i
]
=
m
i
n
(
s
[
k
]
+
g
[
i
]
[
k
]
)
,
k
是
与
s
[
i
]
相
连
的
所
有
的
结
点
s[i]=min(s[k]+g[i][k]),k是与s[i]相连的所有的结点
s[i]=min(s[k]+g[i][k]),k是与s[i]相连的所有的结点
3.自底向上求解最优值
出口就是s[E]=0;
依次往前推
4.构造最优解
输入:9
11
1 2 6
1 3 4
1 4 5
2 5 1
3 5 6
4 6 2
5 7 9
5 8 7
6 8 4
7 9 2
8 9 4
4.2矩阵连乘问题
如 果 给 定 n 个 矩 阵 { A 1 , A 2 , … , A n } , 其 中 A i 与 A i + 1 可 乘 的 。 考 察 n 个 矩 阵 的 连 乘 问 题 。 矩 阵 连 乘 是 可 以 加 括 号 的 A = ( A k A k + 1 ) , 加 括 号 可 以 看 成 是 一 种 次 序 问 题 。 矩 阵 A [ m , n ] 与 矩 阵 B [ n , k ] 相 乘 , 其 计 算 量 是 m ∗ n ∗ k , 矩 阵 连 乘 问 题 要 将 计 算 量 求 为 最 小 。 如果给定n个矩阵\{A_1,A_2,…,A_n\},其中A_i与A_{i+1}可乘的。考察n个矩阵的连乘问题。 \\矩阵连乘是可以加括号的A=(A_kA_{k+1}),加括号可以看成是一种次序问题。\\ 矩阵A[m,n]与矩阵B[n,k]相乘,其计算量是m*n*k,矩阵连乘问题要将计算量求为最小。 如果给定n个矩阵{A1,A2,…,An},其中Ai与Ai+1可乘的。考察n个矩阵的连乘问题。矩阵连乘是可以加括号的A=(AkAk+1),加括号可以看成是一种次序问题。矩阵A[m,n]与矩阵B[n,k]相乘,其计算量是m∗n∗k,矩阵连乘问题要将计算量求为最小。
所 以 次 问 题 转 化 为 了 怎 么 给 矩 阵 连 乘 A 1 A 2 A 3 … A n 在 合 适 的 位 置 加 上 括 号 。 所以次问题转化为了怎么给矩阵连乘A_1A_2A_3…A_n在合适的位置加上括号。 所以次问题转化为了怎么给矩阵连乘A1A2A3…An在合适的位置加上括号。
- 刻画最优子结构性质
可 以 将 该 问 题 的 最 后 一 步 看 作 两 个 矩 阵 相 乘 A = ( B C ) = ( A 1 A 2 A 3 … A k ) ( A k + 1 … A n ) , 利 用 分 治 法 的 思 想 , B , C 可 以 也 分 为 两 个 更 小 的 矩 阵 相 乘 , 即 将 原 问 题 化 成 了 与 原 问 题 类 似 的 子 问 题 , 最 优 子 结 构 性 质 。 A 的 计 算 量 是 B , C 计 算 量 之 和 , 依 次 化 为 更 小 的 子 问 题 . 可以将该问题的最后一步看作两个矩阵相乘A=(BC)=(A_1A_2A_3…A_k)(A_{k+1}…A_n),利用分治法的思想,B,C可以也分为\\两个更小的矩阵相乘,即将原问题化成了与原问题类似的子问题,最优子结构性质。A的计算量是B,C计算量之和,依次化为更小的子问题. 可以将该问题的最后一步看作两个矩阵相乘A=(BC)=(A1A2A3…Ak)(Ak+1…An),利用分治法的思想,B,C可以也分为两个更小的矩阵相乘,即将原问题化成了与原问题类似的子问题,最优子结构性质。A的计算量是B,C计算量之和,依次化为更小的子问题.
- 递归定义最优解
此问题利用递归思想求解,采用分治法,将原问题划分为与它类似的子问题,会出现子问题重叠出现。
递归出口就是只有一个矩阵相乘或者两个(一个矩阵计算量为0,两个矩阵为两个矩阵相乘计算量)
矩
阵
的
参
数
用
p
表
示
{
p
0
,
p
1
,
p
2
…
…
p
n
}
设
矩
阵
A
i
A
i
+
1
…
A
j
的
计
算
量
是
a
[
i
]
[
j
]
,
利
用
数
组
a
来
表
示
计
算
量
,
a
[
i
]
[
j
]
=
{
0
i
=
j
m
i
n
k
=
i
k
<
j
(
a
[
i
]
[
k
]
+
a
[
k
+
1
]
[
j
]
+
p
i
−
1
p
k
p
j
)
i
<
j
b
[
i
]
[
j
]
表
示
求
解
a
[
i
]
[
j
]
时
中
间
的
断
点
k
.
矩阵的参数用p表示\{p_0,p_1,p_2……p_n\}设矩阵A_iA_{i+1}…A_j的计算量是a[i][j],利用数组a来表示计算量,\\a[i][j]=\begin{cases} 0&i=j\\ min_{k=i}^{k<j}(a[i][k]+a[k+1][j]+p_{i-1}p_{k}p_{j})&i<j \end{cases} \\b[i][j]表示求解a[i][j]时中间的断点k.
矩阵的参数用p表示{p0,p1,p2……pn}设矩阵AiAi+1…Aj的计算量是a[i][j],利用数组a来表示计算量,a[i][j]={0mink=ik<j(a[i][k]+a[k+1][j]+pi−1pkpj)i=ji<jb[i][j]表示求解a[i][j]时中间的断点k.
- 自底向上求解最优值(策略选择)
利用数组,自底向上求解。
假
设
A
1
:
7
∗
8
,
A
2
:
8
∗
12
,
A
3
:
12
∗
5
,
A
4
:
5
∗
4
,
A
5
:
4
∗
6
,
求
解
其
最
小
计
算
量
假设A_1:7*8,A_2:8*12,A_3:12*5,A_4:5*4,A_5:4*6,求解其最小计算量
假设A1:7∗8,A2:8∗12,A3:12∗5,A4:5∗4,A5:4∗6,求解其最小计算量
笔算结果:
a[i][j] | 1 | 2 | 3 | 4 | 5 | 求解顺序(按斜线) |
---|---|---|---|---|---|---|
1 | 0 | 672 | 760 | 848 | 1016 | |
2 | 0 | 480 | 624 | 816 | 5 | |
3 | 0 | 240 | 480 | 4 | ||
4 | 0 | 120 | 3 | |||
5 | 0 | 2 | ||||
1 |
b[i][j] | 1 | 2 | 3 | 4 | 5 |
---|---|---|---|---|---|
1 | 0 | 1 | 1 | 1 | 4 |
2 | 0 | 2 | 2 | 4 | |
3 | 0 | 3 | 3 | ||
4 | 0 | 4 | |||
5 | 0 |
结果为:a[1][5]=1016,最优解是:(A1(A2(A3A4)))A5
计算过程:
a
[
1
]
[
2
]
k
=
1
a
[
1
]
[
1
]
+
a
[
2
]
[
2
]
+
p
0
p
1
p
2
=
672.
a
[
2
]
[
3
]
k
=
2
a
[
2
]
[
2
]
+
a
[
3
]
[
3
]
+
p
1
p
2
p
3
=
480.
a
[
3
]
[
4
]
k
=
3
a
[
3
]
[
3
]
+
a
[
4
]
[
4
]
+
p
2
p
3
p
4
=
240.
a
[
4
]
[
5
]
k
=
4
a
[
4
]
[
4
]
+
a
[
5
]
[
5
]
+
p
3
p
4
p
5
=
120.
a
[
1
]
[
3
]
=
m
i
n
{
k
=
1
a
[
1
]
[
1
]
+
a
[
2
]
[
3
]
+
p
0
p
1
p
3
=
760
k
=
2
a
[
1
]
[
2
]
+
a
[
3
]
[
3
]
+
p
0
p
2
p
3
=
1092
a
[
2
]
[
4
]
=
m
i
n
{
k
=
2
a
[
2
]
[
2
]
+
a
[
3
]
[
4
]
+
p
1
p
2
p
4
=
624
k
=
3
a
[
2
]
[
3
]
+
a
[
4
]
[
4
]
+
p
1
p
3
p
4
=
640
a
[
3
]
[
5
]
=
m
i
n
{
k
=
3
a
[
3
]
[
3
]
+
a
[
4
]
[
5
]
+
p
2
p
3
p
5
=
480
k
=
4
a
[
2
]
[
3
]
+
a
[
4
]
[
4
]
+
p
2
p
4
p
5
=
528
a
[
1
]
[
4
]
=
m
i
n
{
k
=
1
a
[
1
]
[
1
]
+
a
[
2
]
[
4
]
+
p
0
p
1
p
4
=
848
k
=
2
a
[
1
]
[
2
]
+
a
[
3
]
[
4
]
+
p
0
p
2
p
4
=
1248
k
=
3
a
[
1
]
[
3
]
+
a
[
4
]
[
4
]
+
p
0
p
3
p
4
=
900
a
[
2
]
[
5
]
=
m
i
n
{
k
=
2
a
[
2
]
[
2
]
+
a
[
3
]
[
5
]
+
p
1
p
2
p
5
=
1056
k
=
3
a
[
2
]
[
3
]
+
a
[
4
]
[
5
]
+
p
1
p
3
p
5
=
840
k
=
4
a
[
2
]
[
4
]
+
a
[
5
]
[
5
]
+
p
1
p
4
p
5
=
816
a
[
1
]
[
5
]
=
m
i
n
{
k
=
1
a
[
1
]
[
1
]
+
a
[
2
]
[
5
]
+
p
0
p
1
p
5
=
1152
k
=
2
a
[
1
]
[
2
]
+
a
[
3
]
[
5
]
+
p
0
p
2
p
5
=
1656
k
=
3
a
[
1
]
[
3
]
+
a
[
4
]
[
5
]
+
p
0
p
3
p
5
=
1090
k
=
4
a
[
1
]
[
4
]
+
a
[
5
]
[
5
]
+
p
0
p
4
p
5
=
1016
a[1][2]\quad k=1\quad a[1][1]+a[2][2]+p_0p_1p_2=672. \\a[2][3]\quad k=2\quad a[2][2]+a[3][3]+p_1p_2p_3=480. \\a[3][4]\quad k=3 \quad a[3][3]+a[4][4]+p_2p_3p_4=240. \\a[4][5]\quad k =4 \quad a[4][4]+a[5][5]+p_3p_4p_5=120. \\a[1][3]=min\begin{cases}k =1 \quad a[1][1]+a[2][3]+p_0p_1p_3=760\\ k=2\quad a[1][2]+a[3][3]+p_0p_2p_3=1092 \end{cases}\\ a[2][4]=min\begin{cases} k=2\quad a[2][2]+a[3][4]+p_1p_2p_4=624\\ k=3\quad a[2][3]+a[4][4]+p_1p_3p_4=640 \end{cases}\\ a[3][5]=min\begin{cases} k=3\quad a[3][3]+a[4][5]+p_2p_3p_5=480\\ k=4\quad a[2][3]+a[4][4]+p_2p_4p_5=528 \end{cases}\\ a[1][4]=min\begin{cases} k=1\quad a[1][1]+a[2][4]+p_0p_1p_4=848\\ k=2\quad a[1][2]+a[3][4]+p_0p_2p_4=1248\\ k=3\quad a[1][3]+a[4][4]+p_0p_3p_4=900 \end{cases}\\ a[2][5]=min\begin{cases} k=2\quad a[2][2]+a[3][5]+p_1p_2p_5=1056\\ k=3\quad a[2][3]+a[4][5]+p_1p_3p_5=840\\ k=4\quad a[2][4]+a[5][5]+p_1p_4p_5=816 \end{cases}\\ a[1][5]=min\begin{cases} k=1\quad a[1][1]+a[2][5]+p_0p_1p_5=1152\\ k=2\quad a[1][2]+a[3][5]+p_0p_2p_5=1656\\ k=3\quad a[1][3]+a[4][5]+p_0p_3p_5=1090\\ k=4\quad a[1][4]+a[5][5]+p_0p_4p_5=1016 \end{cases}\\
a[1][2]k=1a[1][1]+a[2][2]+p0p1p2=672.a[2][3]k=2a[2][2]+a[3][3]+p1p2p3=480.a[3][4]k=3a[3][3]+a[4][4]+p2p3p4=240.a[4][5]k=4a[4][4]+a[5][5]+p3p4p5=120.a[1][3]=min{k=1a[1][1]+a[2][3]+p0p1p3=760k=2a[1][2]+a[3][3]+p0p2p3=1092a[2][4]=min{k=2a[2][2]+a[3][4]+p1p2p4=624k=3a[2][3]+a[4][4]+p1p3p4=640a[3][5]=min{k=3a[3][3]+a[4][5]+p2p3p5=480k=4a[2][3]+a[4][4]+p2p4p5=528a[1][4]=min⎩⎪⎨⎪⎧k=1a[1][1]+a[2][4]+p0p1p4=848k=2a[1][2]+a[3][4]+p0p2p4=1248k=3a[1][3]+a[4][4]+p0p3p4=900a[2][5]=min⎩⎪⎨⎪⎧k=2a[2][2]+a[3][5]+p1p2p5=1056k=3a[2][3]+a[4][5]+p1p3p5=840k=4a[2][4]+a[5][5]+p1p4p5=816a[1][5]=min⎩⎪⎪⎪⎨⎪⎪⎪⎧k=1a[1][1]+a[2][5]+p0p1p5=1152k=2a[1][2]+a[3][5]+p0p2p5=1656k=3a[1][3]+a[4][5]+p0p3p5=1090k=4a[1][4]+a[5][5]+p0p4p5=1016
首先,对角线上的值为0,利用最优解性质求解:
矩
阵
的
参
数
用
p
表
示
{
p
0
,
p
1
,
p
2
…
…
p
n
}
设
矩
阵
A
i
A
i
+
1
…
A
j
的
计
算
量
是
a
[
i
]
[
j
]
,
利
用
数
组
a
来
表
示
计
算
量
,
a
[
i
]
[
j
]
=
{
0
i
=
j
m
i
n
k
=
i
k
<
j
(
a
[
i
]
[
k
]
+
a
[
k
+
1
]
[
j
]
+
p
i
−
1
p
k
p
j
)
i
<
j
矩阵的参数用p表示\{p_0,p_1,p_2……p_n\}设矩阵A_iA_{i+1}…A_j的计算量是a[i][j],利用数组a来表示计算量,\\a[i][j]=\begin{cases} 0&i=j\\ min_{k=i}^{k<j}(a[i][k]+a[k+1][j]+p_{i-1}p_{k}p_{j})&i<j \end{cases}
矩阵的参数用p表示{p0,p1,p2……pn}设矩阵AiAi+1…Aj的计算量是a[i][j],利用数组a来表示计算量,a[i][j]={0mink=ik<j(a[i][k]+a[k+1][j]+pi−1pkpj)i=ji<j
依次按计算顺序求解,填充表格。
void MA(int *p,int n,int ** m,int *s){
for(int i =1;i<=n;i++){
m[i][i]=0;
}
for(int r =2;r<=n;r++){
for(int i =1;i<=n-r+1;i++){
int j =r+i-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 temp=m[i][k]+m[k+1][j]+p[i-1]*p[k]*p[j];
if(temp<m[i][j]){
m[i][j]=temp;
s[i][j]=k;
}
}
}
}
}
void yzhi() {
//i表示每一次求得那条斜线上的第一个元素的列坐标
//j表示此时对应要求的元素的行坐标
for (int i = 2; i <= n; i++) {
for (int j = 1; j <= n - i + 1; j++) {
//此时要求解的是a[j][i+j-1],利用最优解性质
int s = 1e9;
for (int k = j; k < i + j - 1; k++) {
int x = a[j][k] + a[k + 1][i + j - 1] + p[j - 1] * p[k] * p[i + j - 1];
//cout << x << endl;
if (x < s) { s = x; b[j][i + j - 1] = k; }
}
a[j][i + j - 1] = s;
}
}
cout << "最优值为:" << endl;
cout << a[1][n] << endl;
}
- 构造最优解
string yjie(int i ,int j) {
if (i == j) {
string ss="A"+to_string(i);
return ss;
}
int k = b[i][j];
string s1 = yjie(i, k);
string s2 = yjie(k + 1, j);
if (k - i >= 1)s1 = "(" + s1+")";
if (j - k - 1 >= 1)s2 = "(" + s2 + ")";
return s1 + s2;
}
代码得到的最优解和最优值:
与计算结果一致。
4.3Fibnaocci数列
F i b ( n ) = { 1 n = 0 1 n = 1 F i b ( n − 1 ) + F i b ( n − 2 ) n > 1 Fib(n)=\begin{cases}1&n=0\\1&n=1\\Fib(n-1)+Fib(n-2)&n>1\end{cases} Fib(n)=⎩⎪⎨⎪⎧11Fib(n−1)+Fib(n−2)n=0n=1n>1
解题步骤:按照
- 确定dp数组(dp table)以及下标的含义
- 确定递推公式
- dp数组如何初始化
- 确定遍历顺序
- 举例推导dp数组
这个步骤来做动态规划:
1.一般的动态规划求解是利用一维或者二维数组来求解,所以要先明确数组所代表的含义以及下标所代表的含义:
这个题做一个dp数组,dp就表示数列中第i个数,dp[i];
2.递推公式:
dp[i]=dp[i-1]+dp[i-2].
3.dp初始化
dp[0]=1,dp[1]=1
4.遍历顺序:
从前往后,根据递推公式
5.打印数组,跟自己想的一样与否
举个例子n=7
i | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 |
---|---|---|---|---|---|---|---|---|
dp[i] | 1 | 1 | 2 | 3 | 5 | 8 | 13 | 21 |
void fi() {
s[0] = 1, s[1] = 1;
for (int i = 2; i <= n; i++) {
s[i] = s[i - 1] + s[i - 2];
}
}
4.4最长公共子序列
1.问题描述:
最长公共子序列(LCS)问题:给定两个序列X=<x1, x2, …, xm>和Y=<y1, y2, … , yn>,要求找出X和Y的一个最长公共子序列。
2.问题求解:
- 刻画最优子结构性质
- 递归定义最优解
- 自底向上求解最优值(策略选择)
- 构造最优解
- 刻画最优子结构性质
定理: LCS的最优子结构性质
设序列X=<x1, x2, …, xm>和Y=<y1, y2, …, yn>的一个最长公共子序列Z=<z1, z2, …, zk>,
则:
1.若xm=yn,则zk=xm=yn且Zk-1是Xm-1和Yn-1的最长公共子序 列;
2.若xm≠yn且zk≠xm ,则Z是Xm-1和Y的最长公共子序列;
3.若xm≠yn且zk≠yn ,则Z是X和Yn-1的最长公共子序列。
其中Xm-1=<x1, x2, …, xm-1>,Yn-1=<y1, y2, …, yn-1>, Zk-1=<z1, z2, …, zk-1>。
- 递归定义最优解
由最长公共子序列问题的最优子结构性质可知,要找出X=<x1, x2, …, xm>和Y=<y1, y2, …, yn>的最长公共子序列,可按以下方式递归地进行:当xm=yn时,找出Xm-1和Yn-1的最长公共子序列,然后在其尾部加上xm(=yn)即可得X和Y的一个最长公共子序列。当xm≠yn时,必须解两个子问题,即找出Xm-1和Y的一个最长公共子序列及X和Yn-1的一个最长公共子序列。这两个公共子序列中较长者即为X和Y的一个最长公共子序列。
由此递归结构容易看到最长公共子序列问题具有子问题重叠性质。例如,在计算X和Y的最长公共子序列时,可能要计算出X和Yn-1及Xm-1和Y的最长公共子序列。而这两个子问题都包含一个公共子问题,即计算Xm-1和Yn-1的最长公共子序列。
与矩阵连乘积最优计算次序问题类似,我们来建立子问题的最优值的递归关系。用c[i,j]记录序列Xi和Yj的最长公共子序列的长度。其中Xi=<x1, x2, …, xi>,Yj=<y1, y2, …, yj>。当i=0或j=0时,空序列是Xi和Yj的最长公共子序列,故c[i,j]=0。其他情况下,由定理可建立递归关系如下:
- 计算最优值
直接利用上式容易写出一个计算c[i,j]的递归算法,但其计算时间是随输入长度指数增长的。由于在所考虑的子问题空间中,总共只有θ(m*n)个不同的子问题,因此,用动态规划算法自底向上地计算最优值能提高算法的效率。计算最长公共子序列长度的动态规划算法LCS_LENGTH(X,Y)以序列X=<x1, x2, …, xm>和Y=<y1, y2, …, yn>作为输入。输出两个数组c[0…m ,0…n]和b[1…m ,1…n]。其中c[i,j]存储Xi与Yj的最长公共子序列的长度,b[i,j]记录指示c[i,j]的值是由哪一个子问题的解达到的,这在构造最长公共子序列时要用到。最后,X和Y的最长公共子序列的长度记录于c[m,n]中。
int c[N][N],pa[N][N];
//pa来记录路径,c来记录最优值
void dp() {
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= n; j++) {
if (p1[i] == p2[j]) {
c[i][j] = c[i - 1][j - 1] + 1;
pa[i][j] = 1;
}
else {
if (c[i - 1][j] >= c[i][j - 1]) {
c[i][j] = c[i - 1][j];
pa[i][j] = 2;
}
else {
c[i][j] = c[i][j-1];
pa[i][j] = 3;
}
}
}
}
}
- 求解最优解
void LCS(int i,int j) {
if (i == 0 || j == 0) {
return;
}
if (pa[i][j] == 1) {
LCS(i - 1, j - 1);
cout << p1[i] << endl;
}
else if (pa[i][j] == 2) {
LCS(i - 1, j);
}
else if (pa[i][j] == 3) {
LCS(i, j - 1);
}
}
洛谷:
【模板】最长公共子序列
题目描述
给出 1 , 2 , … , n 1,2,\ldots,n 1,2,…,n 的两个排列 P 1 P_1 P1 和 P 2 P_2 P2 ,求它们的最长公共子序列。
输入格式
第一行是一个数 n n n。
接下来两行,每行为 n n n 个数,为自然数 1 , 2 , … , n 1,2,\ldots,n 1,2,…,n 的一个排列。
输出格式
一个数,即最长公共子序列的长度。
样例 #1
样例输入 #1
5 3 2 1 4 5 1 2 3 4 5
样例输出 #1
3
提示
- 对于 50 % 50\% 50% 的数据, n ≤ 1 0 3 n \le 10^3 n≤103;
- 对于 100 % 100\% 100% 的数据, n ≤ 1 0 5 n \le 10^5 n≤105。
4.5多边形问题
- 问题描述:
多边形游戏是一个单人玩的游戏,开始时有一个由n个顶点构成的多边形。每个顶点赋予一个整数值,每条边赋予一个运算符“+”或“*”。所有边依次用整数从1到n编号。
游戏第1步,将一条边删除;
随后n-1步按以下方式操作:
1)选择一条边E以及由E连接着的两个顶点v1和v2;
2)用一个新的顶点取代边E以及由E连接着的两个顶点v1和v2, 将顶点v1和v2的整数值通过边E上的运算得到的结果赋予新顶点。
3)最后,所有边都被删除,游戏结束。游戏的得分就是所剩顶点上整数值。
2.问题求解
- 刻画最优子结构性质
- 递归定义最优解
- 自底向上求解最优值(策略选择)
- 构造最优解
- 刻画最优子结构性质
设所给的多边形的顶点和边的顺时针序列为 op[1],v[1],op[2],v[2],…,op[n],v[n] 其中,op[i]表示第i条边所相应的运算符,v[i]表示第i个顶点上的数值,i=1,2,…,n。 在所给多边形中,从顶点i开始,长度为j(链中有j个顶点)的顺时针链p(i,j)可表示为:v[i],op[i+1],…,v[i+j-1]
如果这条链的最后一次合并运算在op[i+s]处发生(1≤s≤j-1),则可在op[i+s]处将链分割为两个子链p(i, s)和p(i+s,j-s)。
设m1是对子链p(i,s)的任意一种合并方式得到的值,而a,b分别是在所有可能的合并中得到的最小值和最大值。m2是p(i+s,j-s)的任意一种合并方式得到的值,而c和d分别是在所有可能的合并中得到的最小值和最大值。依此定义有:
a≤m1≤b , c≤m2≤d 由于子链p(i,s)和p(i+s,j-s)的合并方式决定了p(i,j)在op[i+s]处0断开后的合并方式,在op[i+s]处合并后其值为:
m=(m1)op[i+s](m2).
1.当op[i+s]=‘+’时,显然有a+c ≤b+d。
换句话说,由链p(i,j)合并的最优性可推出子链p(i,s)和p(i+s,j-s) 的最优性,且最大值对应于子链的最大值,最小值对应于子链 的最小值。
2.当op[i+s]=‘*’时,情况有所不同。
由于v[i]可取负整数,子链的最大值相乘未必能得到主链的最大 值。但我们注意到最大值一定在边界点达到,即 min{ac,ad,bc,bd} ≤m ≤max{ac,ad,bc,bd} 换句话说,主链的最大值和最小值可由子链的最大值和最小值 得到。例如,当m=ac时,最大主链由两个最小子链组成;同理 当m=bd时,最大主链由它的两个最大子链组成。无论哪种情形 发生,由主链的最优性均可推出子链的最优性。 综上所述,可知多边形游戏问题满足最优子结构性质。
- 递归定义最优解
由前面的分析可知,为了求链合并的最大值,必须同时求子链合并的最大值和最小值。因此在整个计算过程中,应同时计算最大值和最小值。
设m[i,j,0]是链p(i,j)合并的最小值,而m[i,j,1]是最大值。若最优合并在op[i+s]处将p(i,j)分为2个长度小于j的子链p(i,s)和p(i+s,j-s),且从顶点i开始的长度小于j的子链的最大值和最小值均已计算出。为叙述方便,记:
a=m[i,s,0],b=m[i,s,1],c=m[i+s,j-s,0],d=m[i+s,j-s,1]。
1)当op[i+s]=‘+’时,m[i,j,0]=a+c m[i,j,1]=b+d
2)当op[i+s]=‘*’时, m[i,j,0]=min{ac,ad,bc,bd} m[i,j,1]=max{ac,ad,bc,bd}
综合(1)和(2),将p(i,j)在op[i+s]处断开的最大值记为maxf(i,j,s),最小值记为minf(i,j,s),则
- 自底向上求解最优解
void MaxMin(int n,int i,int j,int s,int& maxf,int& minf){
int e[4];
int a= dp[i][s][0],b=dp[i][s][1],r=(i+s-1)%n+1,c=dp[r][j-s][0],d=dp[r][j-s][1];
//if(op[i+s]=='+'){错误
if(op[r]=='+'){
maxf = b+d;
minf = a+c;
}
else if(op[r]=='*'){
e[0]=a*c;
e[1]=a*d;
e[2]=b*c;
e[3]=b*d;
maxf=e[0];
minf=e[0];
for(int i =1;i<4;i++){
if(e[i]<minf){
minf=e[i];
}
if(e[i]>maxf){
maxf=e[i];
}
}
}
}
void m(int n){
for(int i =1;i<=n;i++){
dp[i][1][0]=v[i];
dp[i][1][1]=v[i];
}
for(int i =1;i<=n;i++){
for(int j=2;j<=n;j++){
dp[i][j][0]=1e9;
dp[i][j][1]=-1e9;
}
}
int maxf,minf;
for(int j =2;j<=n;j++){
for(int i =1;i<=n;i++){
for(int s=1;s<j;s++){
MaxMin(n,i,j,s,maxf,minf,m,op);
if(dp[i][j][1]<maxf){
dp[i][j][1]=maxf;
}
if(dp[i][j][0]>minf){
dp[i][j][0]=minf;
}
}
}
}
int temp=dp[1][n][1];
for(int i =1;i<=n;i++){
if(dp[i][n][1]>temp){
temp=dp[i][n][1];
}
}
}
4.6最大子段和
int Maxsum(int n,int *a){
int sum=0,b=0;
for(int i =1;i<=n;i++){
if(b>0){
b+=a[i];
}
else{
b=a[i];
}
if(b>sum){
sum=b;
}
}
return sum;
}
4.7 0-1背包问题
问题描述:
给定n种物品和一背包。物品i的重量是Wi,其价值为vi 背包的容量为c。问应如何选择装入背包中的物品,使得装入背包中物品的总价值最大?
在选择装入背包的物品时,对每种物品i只有两种选择,即装入背包或不装入背包。不能将物品i装入背包多次,也不能只装入部分的物品。因此,该问题称为0-1背包问题。
此问题形式化描述是:给定c>0, wi>0, vi>0, 要求找出一个n元的0—1向量(x1, x2, x3,…,xn),xi(0,1),
∑
i
=
1
n
w
i
x
i
≤
c
\sum_{i=1}^{n}wixi\le c
∑i=1nwixi≤c,而且
∑
i
=
1
n
v
i
x
i
\sum_{i=1}^{n}vixi
∑i=1nvixi 达到最大。因此,0—1背包问题是一个特殊的整数划归问题:
m
a
x
∑
i
=
1
n
v
i
x
i
=
{
∑
i
=
1
n
w
i
x
i
≤
c
x
i
∈
{
0
,
1
}
1
≤
i
≤
n
max \sum_{i=1}^nv_ix_i =\begin{cases} \sum_{i=1}^{n}w_ix_i \le c\\ x_i\in \{0,1\}&&1\le i \le n \end{cases}
maxi=1∑nvixi={∑i=1nwixi≤cxi∈{0,1}1≤i≤n
问题求解:
- 刻画最优子结构性质
- 递归定义最优解
- 自底向上求解最优值(策略选择)
- 构造最优解
1.刻画最优子结构性质:
0—1背包问题具有最优子结构性质。设
(
y
1
,
y
2
,
…
,
y
n
)
(y_1,y_2,…,y_n)
(y1,y2,…,yn)是所给0—1背包问题的一个最优解,则
(
y
2
,
y
3
,
…
,
y
n
)
(y_2,y_3,…,y_n)
(y2,y3,…,yn)是下面相应子问题的问题的一个最优解:
m
a
x
∑
i
=
2
n
v
i
x
i
=
{
∑
i
=
2
n
w
i
x
i
≤
c
−
w
1
y
1
x
i
∈
{
0
,
1
}
2
≤
i
≤
n
max \sum_{i=2}^nv_ix_i =\begin{cases}\sum_{i=2}^{n}w_ix_i \le c-w_1y_1\\x_i\in \{0,1\}&&2\le i \le n\end{cases}
maxi=2∑nvixi={∑i=2nwixi≤c−w1y1xi∈{0,1}2≤i≤n
否则,设
(
z
2
,
z
3
,
…
,
z
n
)
(z_2,z_3,…,z_n)
(z2,z3,…,zn)是相应子问题的问题的一个最优解,而
(
y
2
,
y
3
,
…
,
y
n
)
(y_2,y_3,…,y_n)
(y2,y3,…,yn)不是该问题的一个最优解。由此可知,
∑
i
=
2
n
v
i
z
i
>
∑
i
=
2
n
v
i
y
i
\sum_{i=2}^nv_iz_i>\sum_{i=2}^nv_iy_i
∑i=2nvizi>∑i=2nviyi, 且
w
1
y
1
+
∑
i
=
2
n
w
i
z
i
≤
c
w_1y_1+\sum_{i=2}^nw_iz_i\le c
w1y1+∑i=2nwizi≤c.因此,
v
1
y
1
+
∑
i
=
2
n
v
i
z
i
>
∑
i
=
1
n
v
i
y
i
w
i
y
i
+
∑
i
=
2
n
w
i
z
i
≤
c
v_1y_1+\sum_{i=2}^nv_iz_i>\sum_{i=1}^nv_iy_i\quad\quad w_iy_i+\sum_{i=2}^nw_iz_i\le c\\
v1y1+i=2∑nvizi>i=1∑nviyiwiyi+i=2∑nwizi≤c
这说明
(
y
1
,
z
2
,
z
3
,
…
,
z
n
)
(y_1,z_2,z_3,…,z_n)
(y1,z2,z3,…,zn)是该问题的一个更优解,与假设相反,矛盾。
2.递归定义最优解:
设m[i][j]是背包容量为j,可选物品为i,i+1,i+2,…,n时0—1背包问题的最优值。
m
[
i
]
[
j
]
=
{
m
a
x
m
[
i
+
1
]
[
j
]
,
m
[
i
+
1
]
[
j
−
w
[
i
]
]
+
v
[
i
]
)
j
≥
w
[
i
]
m
[
i
+
1
]
[
j
]
j
<
w
[
i
]
m[i][j]=\begin{cases} max{m[i+1][j],m[i+1][j-w[i]]+v[i]})&& j\ge w[i]\\ m[i+1][j]&&j<w[i] \end{cases}
m[i][j]={maxm[i+1][j],m[i+1][j−w[i]]+v[i])m[i+1][j]j≥w[i]j<w[i]
初始化:
m
[
n
]
[
j
]
=
{
v
[
i
]
j
≥
w
[
i
]
0
j
<
w
[
i
]
m[n][j]=\begin{cases} v[i]&&j\ge w[i]\\ 0&& j<w[i] \end{cases}
m[n][j]={v[i]0j≥w[i]j<w[i]
3.自底向上求解最优值
void kn(int n,int c,int ** m,int * w,int * v){
int jMax = min(w[n]-1,c);
for(int j =0;j<=jMax;j++){
m[n][j]=0;
}
for(int j =w[i];j<=c;j++){
m[n][j]=v[n];
}
for(int i =n-1;i>1;i--){
jMax = min(w[i]-1,c);
for(int j =0;j<=jMax;j++){
m[i][j]=m[i+1][j];
}
for(int j = w[i];j<=c;j++){
m[i][j]=max(m[i+1][j],m[i+1][j-w[i]]+v[i]);
}
}
m[1][c]=m[2][c];
if(c>=w[1]){
m[1][c]=max(m[1][c],m[2][c-w[1]]+v[1]);
}
}
void backtrace(int n, int c,int ** m,int * w,int * v,int * x){
for(int i =1;i<n;i++){
if(m[i][c]==m[i+1][c]){
x[i]=0;
}
else{
x[i]=1;
c-=w[i];
}
}
x[n]=(m[n][c])?1:0;
}
算法复杂度:
O(cn)
输入样例:
n = 5,c=10,w={3,2,4,1,6},p={10,7,12,4,20}
结果:
手动核算结果:
i j | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |
---|---|---|---|---|---|---|---|---|---|---|---|
1 | 0 | 4 | 7 | 11 | 14 | 17 | 21 | 24 | 27 | 31 | 34 |
2 | 0 | 4 | 7 | 11 | 12 | 16 | 20 | 24 | 27 | 31 | 32 |
3 | 0 | 4 | 4 | 4 | 12 | 16 | 20 | 24 | 24 | 24 | 32 |
4 | 0 | 4 | 4 | 4 | 4 | 4 | 20 | 24 | 24 | 24 | 24 |
5 | 0 | 0 | 0 | 0 | 0 | 0 | 20 | 20 | 20 | 20 | 20 |
1 | 2 | 3 | 4 | 5 | |
---|---|---|---|---|---|
x | 1 | 0 | 0 | 1 | 1 |
结果一致,正确。
第五章 贪心算法(NP)
5.1活动安排问题
void np(int n,int a[],int s[],int f[]){
a[1]=1;
int k =1;
for(int i =2;i<=n;i++){
if(f[k]<=s[i]){
a[i]=1;
k=i;
}
}
}
5.2最优装载问题
void sort(int v[],int t[],int n){
int b[N];
v[0] = INT_MAX;
for (int i = 1; i <= n; i++) {
int u = 0;
for (int j = 1; j <= n; j++) {
if (v[j] < v[u] && (!b[j])) {
u = j;
}
}
t[i] = u;
b[u] = 1;
}
}
void np(int n,int c,int v[],int x[]){
int t[N];
sort(v,t,n);
for(int i =1;i<=n;i++){
if(v[t[i]]>c){
break;
}
x[t[i]]=1;
c-=v[t[i]];
}
}
5.3单源最短路径
void dijk(int n,int dist[],int c[][],int prev[],int v){
int b[N];
for(int i =1;i<=n;i++){
b[i]=0;
dist[i]=c[v][i];
if(dist[i]!=INT_MAX){
prev[i]=v;
}
else{
prev[i]=0;
}
}
b[v]=1;
dist[v]=0;
//这里是小于n
for(int i =1;i<n;i++){
int temp=INT_MAX;
int u =v;
for(int j =1;j<=n;j++){
if(dist[j]<temp&&(!b[j]){
temp=dist[j];
u = j;
}
}
b[u]=1;
for(int j =1;j<=n;j++){
if(!b[j]){
temp=dist[u]+c[u][j];
if(temp<dist[j]){
dist[j]=temp;
prev[j]=u;
}
}
}
}
}
5.4最小生成树
//prim算法
const int N = INT_MAX;
void prim(int n,int ** c){
int closest[N];
int lowcost[N];
int s[N];
s[1]=1;
for(int i=2;i<=n;i++){
s[i]=0;
closest[i]=1;
lowcost[i]=c[1][i];
}
for(int i =1;i<n;i++){
int min=N;
int j =1;
for(int k =2;k<=n;k++){
if(lowcost[k]<min&&(!s[k])){
min=lowcost[k];
j=k;
}
}
s[j]=1;
for(int k = 2;k<=n;k++){
if(c[j][k]<lowcost[k]&&(!s[k])){
lowcost[k]=c[j][k];
closest[k]=j;
}
}
}
}
O(N^2)