【问题描述】
给定n个活动,其中的每个活动ai包含一个起始时间si与结束时间fi。设计与实现算法从n个活动中找出一个最大的相互兼容的活动子集S。
要求:分别设计动态规划与贪心算法求解该问题。其中,对贪心算法分别给出递归与迭代两个版本的实现。
动态规划版本描述:
下面我们再换个角度考虑上面的问题。很多最优化深搜问题都可以巧妙地转化成动态规划问题,可以转化的根本原因在于存在重复子问题,我们看图四就会发现最多区间调度问题也存在重复子问题,所以可以利用动态规划来解决。假设区间已经排序,可以尝试这样设计递归式:前i个区间的最多不重叠区间个数为dp[i]。dp[i]等于啥呢?我们需要根据第i个区间是否选择这两种情况来考虑。如果我们选择第i个区间,它可能和前面的区间重叠,我们需要找到不重叠的位置k,然后计算最多不重叠区间个数dp[k]+1(如果区间按照开始时间排序,则前i+1个区间没有明确的分界线,我们必须按照结束时间排序);如果我们不选择第i个区间,我们需要从前i-1个结果中选择一个最大的dp[j];最后选择dp[k]+1和dp[j]中较大的。
选择或者不选择第i个区间都需要去查找其他的区间,顺序查找的复杂度为O(n),总共有n个区间,每个区间都需要查找,所以动态规划部分最初的算法复杂度为O(n2),已经从指数级降到多项式级,但是经过后面的优化还可以降到O(n),我们一步步来优化。
可以看出dp[i]是非递减的,这可以通过数学归纳法证明。也即当我们已经求得前i个区间的最多不重叠区间个数之后,再求第i+1个区间时,我们完全可以不选择第i+1个区间,从而使得前i+1个区间的结果和前i个区间的结果相同;或者我们选择第i+1个区间,在不重叠的情况下有可能获得更优的结果。dp[i]是非递减的对我们有什么意义呢?首先,如果我们在计算dp[i]时不选择第i个区间,则我们就无需遍历前i-1个区间,直接选择dp[i-1]即可,因为它是前i-1个结果中最大的(虽然不一定是唯一的),此时伪代码中的dp[j]就变成了dp[i-1]。其次,在寻找和第i个区间不重叠的区间时,我们可以避免顺序遍历。如果我们将dp[i]的值列出来,肯定是这样的:
1,1,…,1,2,2,…,2,3,3,…,3,4……
即dp[i]的值从1开始,顺次递增,每一个值的个数不固定。dp[0]肯定等于1,后面几个区间如果和第0个区间重叠,则的dp值也为1;当出现一个区间不和第0个区间重叠时,其dp值变为2,依次类推。由此我们可以得到一个快速获得不重叠位置的方法:重新开辟一个新的数组,用来保存每一个不同dp值的最开始位置,例如pos[1]=0,pos[2]=3,…。这样我们就可以利用O(1)的时间实现find_nonoverlap_pos函数了,然后整个动态规划算法的复杂度就降为O(n)了。
其实从dp的值我们已经就可以发现一些端倪了:dp值发生变化的位置恰是出现不重叠的位置!再仔细思考一下就会出现一开始提到的贪心算法了。所以可以说,贪心算法是动态规划算法在某些问题中的一个特例。该问题的特殊性在于只考虑区间的个数,也即每次都是加1的操作,后面会看到,如果变成考虑区间的长度,则贪心算法不再适用。
1 package org.xiu68.exp.exp7; 2 3 public class Task{ 4 public int startTime; //开始时间 5 public int endTime; //结束时间 6 7 public Task(int startTime,int endTime){ 8 this.startTime=startTime; 9 this.endTime=endTime; 10 } 11 }
1 package org.xiu68.exp.exp7; 2 3 import java.util.ArrayList; 4 5 public class Exp7_2_1 { 6 public static void main(String[] args) { 7 // TODO Auto-generated method stub 8 ArrayList<Task> tasks=new ArrayList<>(); 9 tasks.add(new Task(1,5)); 10 tasks.add(new Task(2,4)); 11 tasks.add(new Task(3,6)); 12 tasks.add(new Task(5,8)); 13 intervalSchedule(tasks); 14 } 15 16 public static void intervalSchedule(ArrayList<Task> tasks){ 17 //按结束时间从小到大进行排序 18 tasks.sort((t1,t2)->{ 19 if(t1.endTime<=t2.endTime) 20 return -1; 21 else 22 return 1; 23 }); 24 25 Task[] t = new Task[tasks.size()]; 26 tasks.toArray(t); 27 int result = 0; 28 if(t.length>=1){ 29 result=1+intervalRecuisive(t, 0, t.length-1); 30 } 31 System.out.println("最大区间个数为: "+result); 32 } 33 public static int intervalRecuisive(Task[] task,int i,int j){ 34 int m=i+1; 35 //下一个任务与前面的任务不兼容 36 while(m<=j && task[m].startTime<task[i].endTime) 37 m++; 38 //已经没有任务与前面任务兼容 39 if(m<=j) 40 return 1+intervalRecuisive(task, m, j); 41 else 42 return 0; 43 } 44 }
1 package org.xiu68.exp.exp7; 2 3 import java.util.ArrayList; 4 5 public class Exp7_2_2 { 6 7 //区间调度的迭代版本(最大区间个数) 8 public static void main(String[] args) { 9 // TODO Auto-generated method stub 10 ArrayList<Task> tasks=new ArrayList<>(); 11 tasks.add(new Task(6,9)); 12 tasks.add(new Task(6,7)); 13 tasks.add(new Task(5,6)); 14 tasks.add(new Task(4,5)); 15 intervalSchedule(tasks); 16 } 17 18 public static void intervalSchedule(ArrayList<Task> tasks){ 19 //按结束时间从小到大进行排序 20 tasks.sort((t1,t2)->{ 21 if(t1.endTime<=t2.endTime) 22 return -1; 23 else 24 return 1; 25 }); 26 27 int result=0,end=0; 28 for(int i=0;i<tasks.size();i++){ //每次选择结束时间最早的任务 29 if(tasks.get(i).startTime>=end){ //如果任务之间相互兼容 30 result+=1; 31 end=tasks.get(i).endTime; 32 } 33 } 34 System.out.println("最大区间个数为: "+result); 35 } 36 }