文章目录
贪心算法
求解最优化问题的算法通常需要经过一系列步骤,在每个步骤都面临多种选择。比如使用动态规划。
动态规划算法的一般步骤有如下:
- 刻画一个最优解的结构特征
- 递归地定义最优解的值
- 计算最优解的值
- 利用计算出的信息构造一个最优解
贪心算法并并不保证得到最优解。但是对于很多问题可以得到最优解。
常见的贪心算法的应用有Huffman编码,最小生成树算法,单源最短路径的Dijkstra算法。
活动选择问题
假定一个有n个活动的集合 S = { a 1 , a 2 , … , a n } S=\{a_1,a_2,…,a_n\} S={a1,a2,…,an},这些活动使用同一个资源(例如同一个阶梯教室),这个资源在某个时刻只能供一个活动使用。每个活动 a i a_i ai 都有一个开始时间 s i s_i si和一个结束时间 f i f_i fi,其中 0 ≤ s i < f i < ∞ 0 \le s_i <f_i<∞ 0≤si<fi<∞。如果任务被选中,任务ai发生在搬开区间 [ s i , f i ) [s_i,f_i) [si,fi)期间。如果二个活动 a i a_i ai和 a j a_j aj满足 [ s i , f i ) [s_i,f_i) [si,fi)和 [ s j , f j ) [s_j,f_j) [sj,fj)不重叠,则它们是兼容的。
目标:希望选择出一个最大兼容活动集。
假定活动已经按照结束时间的单调递增顺序排序:
可以看到子集
{
a
3
,
a
9
,
a
11
}
\{a_3,a_9,a_{11}\}
{a3,a9,a11}是相互兼容的活动,但不是一个最大子集。
子集
{
a
1
,
a
4
,
a
8
,
a
11
}
\{a_1,a_4,a_8,a_{11}\}
{a1,a4,a8,a11}是一个最大子集,同样另外一个
{
a
2
,
a
4
,
a
9
,
a
11
}
\{a_2,a_4,a_9,a_{11}\}
{a2,a4,a9,a11}最大子集
动态规划,把问题分成二个子问题,将二个子问题的最优解整合成原问题的一个最优解。确定哪些子问题用于最优解时,要考虑几种选择。
贪心算法只需要考虑一种选择(贪心的选择)
验证最优子结构性质
令
S
i
j
S_{ij}
Sij表示在
a
i
a_i
ai结束后开始,在
a
j
a_j
aj开始之前结束的那些活动的集合。求
S
i
j
S_{ij}
Sij的一个最大的相互兼容的活动子集,假定这个子集为
A
i
j
A_{ij}
Aij,包含活动
a
k
a_{k}
ak。由于最优解包含
a
k
a_k
ak,得到二个子问题
S
i
k
S_{ik}
Sik(
a
i
a_i
ai结束后开始切
a
k
a_k
ak开始之前结束的那些活动)
S
k
j
S_{kj}
Skj(
a
k
a_{k}
ak结束后开始切
a
j
a_{j}
aj开始之前结束的那些活动)
即
A
i
j
=
A
i
k
∪
a
k
∪
A
k
j
A_{ij}=A_{ik}∪{a_k} ∪A_{kj}
Aij=Aik∪ak∪Akj,活动数目
∣
A
i
j
∣
=
∣
A
i
k
∣
+
∣
A
k
j
∣
+
1
|A_{ij}|=|A_{ik}|+|A_{kj}|+1
∣Aij∣=∣Aik∣+∣Akj∣+1
动态规划的递归式
c
[
i
,
j
]
=
{
0
,
if
S
i
j
=
∅
m
a
x
{
c
[
i
,
k
]
+
c
[
k
,
j
]
+
1
}
,
if
S
i
j
≠
∅
c[i,j]=\begin{cases} 0, & \text{if $S_{ij}$=$\varnothing$}\\ max\{c[i,k]+c[k,j]+1\}, & \text{if $S_{ij}$ $\neq$ $\varnothing$} \end{cases}
c[i,j]={0,max{c[i,k]+c[k,j]+1},if Sij=∅if Sij ̸= ∅
C
[
i
,
j
]
C[i,j]
C[i,j]表示集合大小
动态规划每一次分解为子问题,会考虑所有的活动。如果无需考虑所有子问题就可以选着出一个活动加入到最优解。
贪心选择:只考虑一个选择。
一种贪心策略是:尽可能选择最早结束的那一个,把剩下的资源提供给它之后的尽量多的活动。
证明最早结束的活动总是最优解的一部分
证明非空子问题
S
k
S_k
Sk,令
a
m
a_m
am是
S
k
S_k
Sk中结束时间最早的活动,则
a
m
a_m
am在
S
k
S_k
Sk的某个最大兼容活动子集中。
证:
令
A
k
A_k
Ak是
S
k
S_k
Sk的一个最大兼容活动子集。
A
j
A_j
Aj是
A
k
A_k
Ak中结束时间最早的活动。
1.若
a
j
=
a
m
a_j=a_m
aj=am,已近证明am在Sk的某个最大兼容活动子集。
2.若
a
j
≠
a
m
a_j≠a_m
aj̸=am,令集合
S
k
′
=
(
A
k
−
a
j
)
∪
a
m
S_k^′=(A_k−{a_j })∪{a_m}
Sk′=(Ak−aj)∪am,将
A
k
A_k
Ak中的
a
j
a_j
aj替换为
a
m
a_m
am,
∣
A
k
′
∣
=
∣
A
k
∣
|A_k^′ |=|A_k|
∣Ak′∣=∣Ak∣ ,可以得出结论
A
k
′
A_k^′
Ak′ 也是
S
k
S_k
Sk的一个最大兼容活动子集,并且它包含
a
m
a_m
am。
递归贪心算法
活动选择算法不必使用递归算法,可以自顶向下计算,选择一个活动放入最优解,然后对剩余子问题进行求解。
s开始时间,f结束时间,k求解得子问题,n问题的规模
一开始调用RECURSIVE-ACTIVITY-SELECTOR(s,f,0,n)
迭代贪心算法
时间复杂度为O(n)
贪心算法原理
贪心算法通过做出一系列选择来求出问题的最优解。在每个决策点,它做出在当时来看最佳的选择。这种策略并不能保证总能找到最优解。
基本步骤
- 确定问题的最优子结构
- 设计一个递归算法(活动选择问题,给出了递归式)
- 证明如果我们做出一个贪心选择,则只剩下一个子问题
- 证明贪心选择总是安全的
- 设计一个递归算法实现贪心策略
- 把递归算法转换为迭代算法
更加一般步骤:
- 最优化转化成:对其做出一次选择后,只剩下一个子问题求解。
- 证明做出贪心选择后,原问题总是存在最优解,即贪心选择总是安全的。
- 证明做出贪心选择后,剩余子问题满足性质:其最优解与贪心选择组合即可以得到原问题的最优解。
如何证明一个贪心算法时候能够求解一个最优化问题?
并没有适合所有情况的方法。
一般情况下如果条件有贪心选择和最优子结构这二个特征,就可以向贪心算法靠拢了。
使用整数规划求解最优解问题
首先对每个活动构造0,1变量,x1,x2,…,xn
目标函数为:
Max(x1 + x2 + … + xn)
约束为f(i)<=s(j),i < j
lpsolve代码
int SolveTest2(vector<int>& s, vector<int> f)
{
lprec *lp;
int Ncol, *colno = NULL, j, ret = 0;
REAL *row = NULL;
Ncol = s.size();
lp = make_lp(0, Ncol);
if (lp == nullptr)
{
ret = 1;
}
if (ret == 0)
{
char name[20];
for (int i = 1; i <= s.size(); ++i)
{
string s = "x" + std::to_string(i);
strcpy_s(name, s.c_str());
set_col_name(lp, i, name);
}
colno = (int*)malloc((Ncol+1) * sizeof(*colno));
row = (REAL*)malloc((Ncol+1) * sizeof(*row));
if (colno == nullptr || row == nullptr)
{
ret = 2;
}
}
// max:x1+x2+..xn
int count = 0;
for (int i = 1; i <= Ncol; ++i)
{
colno[count] = i;
row[count] = 1;
count++;
}
set_obj_fnex(lp, count, row, colno);
set_maxim(lp);
//x1,x2,..xn 零一变量
for (int i = 1; i <= Ncol; ++i)
{
set_binary(lp, i, true);
}
int ssize = s.size();
//添加约束
for (int i = 0; i < ssize; ++i)
{
for (int j = i + 1; j < ssize; ++j)
{
if (f[i] > s[j])
{
count = 0;
colno[count] = i + 1;
row[count++] = 1;
colno[count] = j + 1;
row[count++] = 1;
add_constraintex(lp, count, row, colno, LE, 1);
}
}
}
set_add_rowmode(lp, FALSE);
write_LP(lp, stdout);
/* write_lp(lp, "model.lp"); */
/* I only want to see important messages on screen while solving */
set_verbose(lp, IMPORTANT);
/* Now let lpsolve calculate a solution */
ret = solve(lp);
if (ret == OPTIMAL)
ret = 0;
else
ret = 5;
if (ret == 0) {
/* a solution is calculated, now lets get some results */
/* objective value */
printf("Objective value: %f\n", get_objective(lp));
/* variable values */
get_variables(lp, row);
for (j = 0; j < Ncol; j++)
printf("%s: %f\n", get_col_name(lp, j + 1), row[j]);
/* we are done now */
}
/* free allocated memory */
if (row != NULL)
free(row);
if (colno != NULL)
free(colno);
if (lp != NULL) {
delete_lp(lp);
}
return(ret);
}
结果为
/* Objective function */
max: +x1 +x2 +x3 +x4 +x5 +x6 +x7 +x8 +x9 +x10 +x11;
/* Constraints */
+x1 +x2 <= 1;
+x1 +x3 <= 1;
+x1 +x5 <= 1;
+x1 +x10 <= 1;
+x2 +x3 <= 1;
+x2 +x5 <= 1;
+x2 +x10 <= 1;
+x3 +x4 <= 1;
+x3 +x5 <= 1;
+x3 +x6 <= 1;
+x3 +x10 <= 1;
+x4 +x5 <= 1;
+x4 +x6 <= 1;
+x4 +x7 <= 1;
+x4 +x10 <= 1;
+x5 +x6 <= 1;
+x5 +x7 <= 1;
+x5 +x8 <= 1;
+x5 +x9 <= 1;
+x5 +x10 <= 1;
+x6 +x7 <= 1;
+x6 +x8 <= 1;
+x6 +x9 <= 1;
+x6 +x10 <= 1;
+x7 +x8 <= 1;
+x7 +x9 <= 1;
+x7 +x10 <= 1;
+x8 +x9 <= 1;
+x8 +x10 <= 1;
+x9 +x10 <= 1;
+x10 +x11 <= 1;
/* Variable bounds */
x1 <= 1;
x2 <= 1;
x3 <= 1;
x4 <= 1;
x5 <= 1;
x6 <= 1;
x7 <= 1;
x8 <= 1;
x9 <= 1;
x10 <= 1;
x11 <= 1;
/* Integer definitions */
int x1,x2,x3,x4,x5,x6,x7,x8,x9,x10,x11;
Objective value: 4.000000
x1: 1.000000
x2: 0.000000
x3: 0.000000
x4: 1.000000
x5: 0.000000
x6: 0.000000
x7: 0.000000
x8: 0.000000
x9: 1.000000
x10: 0.000000
x11: 1.000000