贪心与回溯与DP
一.贪心
贪心算法(又称贪婪算法)是指,在对问题求解时,总是做出在当前看来是最好的选择。也就是说,不从整体最优上加以考虑,他所做出的仅是在某种意义上的局部最优解。贪心算法不是对所有问题都能得到整体最优解,但对范围相当广泛的许多问题他能产生整体最优解或者是整体最优解的近似解。
它采用自顶向下,以迭代的方法做出相继的贪心选择,每做一次贪心选择就将所求问题简化为一个规模更小的子问题,通过每一步贪心选择,可得到问题的一个最优解,虽然每一步上都要保证能获得局部最优解,但由此产生的全局解有时不一定是最优的,所以贪婪法不要回溯。
贪婪算法是一种改进了的分级处理方法。其核心是根据题意选取一种量度标准。然后将这多个输入排成这种量度标准所要求的顺序,按这种顺序一次输入一个量。如果这个输入和当前已构成在这种量度意义下的部分最佳解加在一起不能产生一个可行解,则不把此输入加到这部分解中。这种能够得到某种量度意义下最优解的分级处理方法称为贪婪算法。
1.特性
2.步骤
3.例子
活动安排问题
问题表述:设有n个活动的集合E = {1,2,…,n},其中每个活动都要求使用同一资源,如演讲会场等,而在同一时间内只有一个活动能使用这一资源。每个活i都有一个要求使用该资源的起始时间si和一个结束时间fi,且si < fi 。如果选择了活动i,则它在半开时间区间[si, fi)内占用资源。若区间[si, fi)与区间[sj, fj)不相交,则称活动i与活动j是相容的。也就是说,当si >= fj或sj >= fi时,活动i与活动j相容。
怎么样安排尽量多的活动?
贪心解决:贪心策略是选取时间结束最早的事情做。根据相交把不可行的解排除掉。
二.回溯
例子八皇后问题:
在8X8格的国际象棋上摆放八个皇后,使其不能互相攻击,即任意两个皇后都不能处于同一行、同一列或同一斜线上,问有多少种摆法。
int vis[3][16]; //因为是逐行放置的,则皇后肯定不会横向攻击,所以只需检测纵向和斜向攻击
//vis[0]表示纵向是否有冲突
//vis[1]表示左斜对角线是否有冲突
//vis[2]表示右斜对角线是否有冲突
int tot; //解个数
int C[8];//解的放置方法
void search(int cur){
int i,j;
if(cur==8) tot++; //递归边界
else for (int i = 0; i < 8; i++)
{
if (!vis[0][i] && !vis[1][cur+i] && !vis[2][cur-i+8]) //剪枝,关键在于如何构建全局变量来剪枝!
{
C[cur]=i; //把cur行的皇后放在i列,如何不用打印解,此C可舍弃
vis[0][i]=vis[1][cur+i]=vis[2][cur-i+8]=1;//修改列和对角线有冲突的位置
search(cur+1);
vis[0][i]=vis[1][cur+i]=vis[2][cur-i+8]=0;//切记!一定要改回来!!
}
}
}
例子二:数组中寻找和为定值的多个数
1 /** 2 * 输入t, r, 尝试Wk 3 */ 4 void sumofsub(int t, int k ,int r, int& M, bool& flag, bool* X) 5 { 6 X[k] = true; // 选第k个数 7 if (t + k == M) // 若找到一个和为M,则设置解向量的标志位,输出解 8 { 9 flag = true; 10 for (int i = 1; i <= k; ++i) 11 { 12 if (X[i] == 1) 13 { 14 printf("%d ", i); 15 } 16 } 17 printf("/n"); 18 } 19 else 20 { // 若第k+1个数满足条件,则递归左子树 21 if (t + k + (k+1) <= M) 22 { 23 sumofsub(t + k, k + 1, r - k, M, flag, X); 24 } 25 // 若不选第k个数,选第k+1个数满足条件,则递归右子树 26 if ((t + r - k >= M) && (t + (k+1) <= M)) //r的作用就在这里!如果剩余的总和也不够,就剪枝掉 27 { 28 X[k] = false; 29 sumofsub(t, k + 1, r - k, M, flag, X); 30 } 31 } 32 } 33 34 void search(int& N, int& M) 35 { 36 // 初始化解空间 37 bool* X = (bool*)malloc(sizeof(bool) * (N+1)); 38 memset(X, false, sizeof(bool) * (N+1)); 39 int sum = (N + 1) * N * 0.5f; 40 if (1 > M || sum < M) // 预先排除无解情况 41 { 42 printf("not found/n"); 43 return; 44 } 45 bool f = false; 46 sumofsub(0, 1, sum, M, f, X); 47 if (!f) 48 { 49 printf("not found/n"); 50 } 51 free(X); 52 } 53 54 int main() 55 { 56 int N, M; 57 printf("请输入整数N和M/n"); 58 scanf("%d%d", &N, &M); 59 search(N, M); 60 return 0; 61 }
三.DP动态规划
把多阶段过程转化为一系列单阶段问题,利用各阶段之间的关系,逐个求解,创立了解决这类过程优化问题的新方法——动态规划。
如果问题是由交叠的子问题所构成,我们就可以用动态规划技术来解决它,一般来说,这样的子问题出现在对给定问题求解的递推关系中,这个递推关系包含了相同问题的更小子问题的解。动态规划法建议,与其对交叠子问题一次又一次的求解,不如把每个较小子问题只求解一次并把结果记录在表中(动态规划也是空间换时间的),这样就可以从表中得到原始问题的解。
步骤:
(1)划分阶段:按照问题的时间或空间特征,把问题分为若干个阶段。在划分阶段时,注意划分后的阶段一定要是有序的或者是可排序的,否则问题就无法求解。
(2)确定状态和状态变量:将问题发展到各个阶段时所处于的各种客观情况用不同的状态表示出来。当然,状态的选择要满足无后效性。
(3)确定决策并写出状态转移方程:因为决策和状态转移有着天然的联系,状态转移就是根据上一阶段的状态和决策来导出本阶段的状态。所以如果确定了决策,状态转移方程也就可写出。但事实上常常是反过来做,根据相邻两段各状态之间的关系来确定决策。
(4)寻找边界条件:给出的状态转移方程是一个递推式,需要一个递推的终止条件或边界条件。
例子:
01背包是在M件物品取出若干件放在空间为W的背包里,每件物品的体积为W1,W2……Wn,与之相对应的价值为P1,P2……Pn。求出获得最大价值的方案。
装箱问题
#include <cstdio>
int
v,n,i,j,k,a[31];
bool
f[20001]; //保存所有状态,f[k]表示装了k容量
int
main(){
scanf
(
"%d%d"
, &v, &n);
for
(
int
i = 1; i <= n; i ++ )
scanf
(
"%d"
, a + i);
f[0] = 1;
for
(
int
i = 1; i <= n; i ++ )
for
(
int
j = v; j >= a[i]; j -- )
if
(!f[j] && f[j - a[i]]) f[j] = 1; //查看已有状态,是否含有f[j - a[i]]的状态
k = v;
for
(; k > 1 && !f[k]; k -- );
printf
(
"%d\n"
, v - k);
}
参考:百科