最优化问题是计算机领域的一个很重要的问题,很多现实的问题本质上都是最优化问题,或者说都可以转化为最优化的问题。比如说怎么规划旅游线路最省钱,在指定的时间里做更多的事情等等,这些都是最优化问题。为了解决最优化问题,计算机界提出了各种算法。
其中有五大常用算法,它们是贪婪算法,动态规划算法,分治算法,回溯算法以及分支限界算法。
1) 贪婪算法
在对问题求解时,总是做出在当前看来是最好的选择。也就是说,不从整体最优上加以考虑,它所做出的是在某种意义上的局部最优解。可以通过一系列局部最优的选择,扩展到整体比较满意的解。贪婪算法可以获取到问题的局部最优解,不一定能获取到全局最优解,同时获取最优解的好坏要看贪婪策略的选择。
例如背包问题中,我们放物品可以选择按物品价值最大的先放;可以选择按重量最小的物品来放;也可以选择单位重量的价值最大来放。
物品类:
1 /// <summary> 2 /// 物品类用于存储物品的属性 3 /// </summary> 4 public class Goods{ 5 6 //编号 7 public int flag; 8 //重量 9 public int weight; 10 //价值 11 public int value; 12 //单位重量 13 public double vw; 14 15 /// <summary> 16 /// goods的构造函数 17 /// </summary> 18 /// <param name="flag">编号</param> 19 /// <param name="weight">重量</param> 20 /// <param name="value">价值</param> 21 /// <param name="vw">单位重量价值</param> 22 public Goods(int flag, int weight, int value, double vw) 23 { 24 this.flag = flag; 25 this.weight = weight; 26 this.value = value; 27 this.vw = vw; 28 } 29 }
排序策略:
1 /// <summary> 2 /// 排序算法--将单位重量的价值,降序排列,利用堆排序 3 /// </summary> 4 /// <param name="goods">物品集</param> 5 /// <returns>单位重量价值,降序的物品集</returns> 6 public Goods[] Sort(Goods[] goods ) 7 { 8 for (int i = goods.Length/2; i >=1; i--) 9 { 10 //筛选:使得二叉树结构中,每一个父节点,都比孩子节点小 11 sift(goods, i, goods.Length); 12 } 13 for (int j = goods.Length; j>=1; j--) 14 { 15 //交换两个元素,第一个位置,已是最小的元素 16 Goods[] temp = new Goods[1]; 17 temp[0] = goods[0]; //取出第一个位置的元素存起来 18 goods[0]= goods[j-1]; //第一个位置,放最后一个元素 19 goods[j - 1] = temp[0]; //最后一个位置,放入第一个元素对象 20 sift(goods, 1, j-1); //进行一次筛选,使得第一个位置的数最小 21 } 22 return goods; 23 } 24 25 /// <summary> 26 /// 堆排序的筛选过程 27 /// </summary> 28 /// <param name="arr">数组</param> 29 /// <param name="i">第i位置(放较小的数)</param> 30 /// <param name="n">数组的个数</param> 31 public void sift(Goods[] goods, int k,int n) 32 { 33 int i = k; 34 int j = 2 * i; 35 Goods[] tempGoods = new Goods[1]; 36 tempGoods[0]= goods[k - 1]; 37 38 if (j<=n) 39 { 40 if (j<n && goods[j-1].vw > goods[j+1-1].vw) //比较子孩子们 41 { 42 j++; 43 } 44 if (goods[k-1].vw > goods[j-1].vw) //比较父节点和较小的一个孩子 45 { 46 goods[i-1] = goods[j-1]; //在第i个位置放入j个位置放的对象 47 i = j; //子节点变为父节点,递归的意思 48 j = 2 * i; 49 50 } 51 } 52 goods[i-1] = tempGoods[0];//之前存起来的数,找到了她的位置 53 54 }
贪心算法解背包问题:
1 /// <summary> 2 /// 贪心算法 3 /// </summary> 4 /// <param name="W">背包的总容量</param> 5 /// <param name="goods">物品</param> 6 /// <returns></returns> 7 public float[] GreedyKnapsack(int W,Goods[] goods) 8 { 9 10 //状态,0表示不在,1表示在,0-1表示部分在 11 float[] state = new float[5]; 12 13 int i; 14 15 //初始化状态 16 for ( i = 1; i <= goods.Length; i++) 17 { 18 state[i - 1] = 0; 19 } 20 21 for ( i = 1; i <= goods.Length; i++) 22 { 23 if (goods[i-1].weight<=W) //当整个物品可以放下时 24 { 25 state[i - 1] = 1; 26 value = value + goods[i - 1].value; 27 W = W - goods[i - 1].weight; 28 } 29 else 30 { 31 break; //当整个物品放不进去,直接跳出整个循环,后面的不再判断 32 } 33 } 34 //只有部分物品可以放进去 35 if (i<=goods.Length) 36 { 37 state[i - 1] = W/(float)goods[i - 1].weight; 38 value = value + goods[i - 1].value * state[i - 1]; 39 } 40 return state; 41 }
调用:
Goods[] goods = new Goods[5] {new Goods(1,30,65,2.1),new Goods(2,50,60,1.2),new Goods(3,40,40,1) ,new Goods(4,20,30,1.5), new Goods(5,10,20,2)}; Goods[] newGoods = new Goods[5]; float[] state = new float[5]; //得到goods按照单位重量价值降序排列的newGoods newGoods = Sort(goods); state = GreedyKnapsack(100, goods); int value = 0; for (int i = 1; i <= state.Length; i++) { value = value + state[i-1]*newGoods[i-1].value; } Console.WriteLine("背包的总价值为:" + value);
2) 动态规划算法
当最优化问题具有重复子问题和最优子结构的时候,就是动态规划出场的时候了。动态规划算法的核心就是提供了一个memory来缓存重复子问题的结果,避免了递归的过程中的大量的重复计算。动态规划算法的难点在于怎么将问题转化为能够利用动态规划算法来解决。当重复子问题的数目比较小时,动态规划的效果也会很差。
如:求斐波那契数列第n项的值
1 private int FastFib(int n, Dictionary<int, int> memo) 2 { 3 if(!memo.ContainsKey(n)) 4 { 5 memo.Add(n, FastFib(n - 1, memo) + FastFib(n - 2, memo)); 6 } 7 return memo[n]; 8 } 9 10 private int FibL(int n) 11 { 12 Dictionary<int, int> memo = new Dictionary<int, int>(); 13 memo.Add(0, 1); 14 memo.Add(1, 1); 15 return FastFib(n, memo); 16 } 17 18 // 调用获取第6项的值 19 int n = 6; 20 int res = Fib(n);
3) 分治算法
分治算法的逻辑更简单了,就是一个词,分而治之。分治算法就是把一个大的问题分为若干个子问题,然后在子问题继续向下分,一直到base cases,通过base cases的解决,一步步向上,最终解决最初的大问题。分治算法是递归的典型应用。
分治法所能解决的问题一般具有以下几个特征:
1) 该问题的规模缩小到一定的程度就可以容易地解决。
2) 该问题可以分解为若干个规模较小的相同问题,即该问题具有最优子结构性质。
3) 利用该问题分解出的子问题的解可以合并为该问题的解。
4) 该问题所分解出的各个子问题是相互独立的,即子问题之间不包含公共的子子问题。
第一条特征是绝大多数问题都可以满足的,因为问题的复杂性一般是随着问题规模的增加而增加;
第二条特征是应用分治法的前提它也是大多数问题可以满足的,此特征反映了递归思想的应用;、
第三条是关键,能否利用分治法完全取决于问题是否具有第三条特征,如果具备了第一条和第二条特征,而不具备第三条特征,则可以考虑用贪心法或动态规划法。
第四条特征涉及到分治法的效率,如果各子问题是不独立的则分治法要做许多不必要的工作,重复地解公共的子问题,此时虽然可用分治法,但一般用动态规划法较好。
4) 回溯算法
回溯算法是深度优先策略的典型应用,回溯算法就是沿着一条路向下走,如果此路不同了,则回溯到上一个分岔路,在选一条路走,一直这样递归下去,直到遍历完所有的路径。
八皇后问题:在8X8格的国际象棋上摆放八个皇后,使其不能互相攻击,即任意两个皇后都不能处于同一行、同一列或同一斜线上,问有多少种摆法。
用回溯算法解决八皇后问题,需要考虑以下几点:
1)将8个皇后定义为8行中的相对位置来标识,考虑增加新的皇后时,是否与之前的皇后位置冲突(即可以攻击之前摆放的皇后:位置相等或者斜率1or-1)
2)新放的皇后发生冲突时回溯至上一行继续试探,逐步回溯直至第一行为止
3)已经求出的解再次探索时避免重复
4)从第一行开始放皇后,然后开始循环往下放,可以设计为回调放皇后的方法
1 class Program 2 { 3 int sum = 0; 4 int[] queues = new int[8]; 5 6 static void Main(string[] args) 7 { 8 Program pro = new Program(); 9 pro.QueueSort(0); 10 Console.WriteLine("Hello World!"); 11 } 12 13 public void QueueSort(int num) 14 { 15 for (int j = 1; j < 9; j++) 16 { 17 if(num == 8) 18 { 19 sum++; 20 Write(); 21 break; 22 } 23 if(FooConflict(num, j)) 24 { 25 queues[num] = j; 26 QueueSort(num + 1); 27 } 28 } 29 } 30 public bool FooConflict(int row, int queen) 31 { 32 if(row == 0) 33 { 34 return true; 35 } 36 else 37 { 38 for(int pointer=0; pointer<row;pointer++) 39 { 40 if(!FooCompare(queues[pointer],row-pointer,queen)) 41 { 42 return false; 43 } 44 } 45 return true; 46 } 47 } 48 49 public bool FooCompare(int i, int row, int queen) 50 { 51 if((i==queen)||(i-queen)==row || (queen-i) == row) 52 { 53 return false; 54 } 55 return true; 56 } 57 58 public void Write() 59 { 60 Console.WriteLine("第{0}个皇后排列:", sum); 61 for(int i=0; i < 8; i++) 62 { 63 for(int j=1; j< 9; j++) 64 { 65 if(j == queues[i]) 66 { 67 System.Console.Write("★"); 68 } 69 else 70 { 71 System.Console.Write("☆"); 72 } 73 } 74 System.Console.Write(" "); 75 } 76 } 77 78 }
5) 分支限界算法
回溯算法是深度优先,那么分支限界法就是广度优先的一个经典的例子。但回溯法是找出问题的许多解,而分支限界法是找出原问题的一个解。或是在满足约束条件的解中找出使某一目标函数值达到极大或极小的解,即在某种意义下的最优解。
*****************************************************
*** No matter how far you go, looking back is also necessary. ***
*****************************************************
……