贪心算法学习


贪心算法

贪心算法的基本要素

  • 最优子元素
  • 贪心选择性质

贪心算法的设计要素

  • 适用于组合优化问题
  • 求解过程是多步判断的过程,最终的判断序列对应于问题的最优解
  • 判断依据某种“短视的”贪心选择性质,性质的好坏决定了算法的成败
  • 贪心法必须进行严格的证明

贪心算法的应用

贪心算法在许多优化算法中有广泛运用:

  • 最小生成树Prime算法
  • Kruskal算法
  • 单源最短路径的Dijkstra算法
  • 数据压缩的Huffman算法等

由一个简单的例子来介绍:

活动安排问题

【问题描述】

设有 \(n\) 个活动的集合 \(E={1,2,…,n}\),其中每个活动都要求使用同一资源,如演讲会场等,而在同一时间内只有一个活动能使用这一资源。每个活动 \(i\) 都有一个要求使用该资源的起始时间 \(S_i\) 和一个结束时间 \(F_i\),且 \(S_i

如果选择了活动 \(i\),则它在时间区间 \([S_i,F_i)\) 内占用资源。若区间 \([S_i,F_i)\) 与区间 \([S_j,F_j)\) 不相交,则称活动 \(i\) 与活动 \(j\) 是相容的。也就是说,当 \(F_i≤S_j\)\(F_j ≤ S_i\)时,活动 \(i\) 与活动 \(j\) 相容。选择出由互相兼容的活动组成的最大集合。

【输入格式】

第一行一个整数 \(n\)

接下来的 \(n\) 行,每行两个整数 \(s_i\)\(f_i\)

【输出格式】

输出互相兼容的最大活动个数。

【输入样例】

4
1 3
4 6
2 5
1 7

【输出样例】

2

定义声明变量:

  • st[i]来存储是否选中了活动 \(i\)
  • Act[i].start 用来存储第 \(i\) 个活动的开始时间
  • Act[i].end 用来存储第 \(i\) 个活动的结束时间
  • \(j\) 表示最近一次加入选中的活动
  • cnt存储选中活动的数目
解题步骤
  • 步骤一:

    • 用结构体数组来存储活动起始时间,优点是方便按照活动结束时间对活动进行排序。
    struct Activity
    {
    	int start ;
    	int end ;
    }Act[1005];
    
  • 步骤二:

    • 对活动按照终止时间从小到大进行排序
    bool cmp(Activity a,Activity b){
    	return a.end
  • 步骤三

    • 决定是否选择下一个活动,当下一个活动的开始时间小于等于选中的最晚时间时,将这下一个活动选中,并更新j
    for(int i = 2 ; i < n ; i ++){
    		if(Act[i].start >= Act[j].end){
    			cnt ++ ;
    			st[j] = true ;
    			j = i ;
    		}
    	}
    
源码
#include
using namespace std;
const int N = 1e5+10 ;
bool st[N] ;
struct Activity
{
	int start ;
	int end ;
}Act[1005];

bool cmp(Activity a,Activity b){
	return a.end> n ;
	for(int i = 1 ; i <= n ; i ++){
		cin >> Act[i].start >> Act[i].end ;
	}
	sort(Act+1,Act+1+n,cmp);
	int j = 1 ;
	st[1] = true ;
	int cnt = 1 ;
	for(int i = 2 ; i < n ; i ++){
		if(Act[i].start >= Act[j].end){
			cnt ++ ;
			st[j] = true ;
			j = i ;
		}
	}
	cout << cnt ;
	return 0;
}

最小延迟调度

【问题描述】

给定等待服务的客户集合\(A=\){\(1,2,… , n\)}, 预计对客户 \(i\) 的服务时间是 \(t_i\) , 该客户希望的完成时间是 \(d_i\) , 即\(T=, D=\). 如果对客户\(i\) 的服务在 \(d_i\)之前结束, 那么对客户 \(i\) 的服务没有延迟;如果在 $d_i $ 之后结束, 那么这个服务就被延迟了, 延迟的时间等于该服务结束时间减去 \(d_i\) . 假设\(t_i\), \(d_i\)都是正整数, 一个调度函数为\(f\) ,$ f(i) $为对客户 \(i\) 的服务开始的时间, 要求所有区间\((f(i), f(i) + t_i)\)互不重叠. 一个调度 $f $ 的最大延迟是所有客户延迟时间的最大值. 求最大延迟达到最小的调度 \(f\).

【输入形式】客户数 \(n\),各客户服务时间及期望完成时间
【输出形式】最小延迟时间调度安排及延迟时间
【样例输入】

5
5 8 4 10 3
10 12 15 11 20

【样例输出】

1 4 2 3 5
12

求最大延迟达到的最小调度就是求 \(f\) 使得

\[\min_{f}{\max_{i\in A}{(f(i)+t_i-d_i))}} \]

样例解释
  • 客户集合:A = {1 , 2 , 3 , 4 , 5}
  • 服务时间集合:T = {5 , 8 , 4 , 10 , 3}
  • 截止时间:D = {10 , 12 , 15 , 11 , 20}

假设调度安排按照顺序

不难得出\(f_1(1) = 0 ,f_1(2) = 5,f_1(3)=13,f_1(4) = 17,f_1(5) = 27\)

各个任务延迟:\(0,1,2,16,10\)

最大延迟为 :\(16\)

我们的任务就是在所有的调度情况下找到最大延迟最小的那种情况,并输出其最大延迟与分配调度。

定义声明变量:

  • now来存储选择某方案情况下,服务时间之和。 在\(now\) 的更新过程中,也是对最大延迟时间的更新。
  • ans 用来存储最大延迟时间
解题步骤
  • 步骤一:

    • 用结构体数组来存储活动起始时间,优点是方便按照活动结束时间对活动进行排序。
    struct node{
        int first,second;
        int id;// 这里的id代表客户的num
    }arr[maxn];
    
  • 步骤二:

    • 对活动按照终止时间从小到大进行排序
    bool cmp(node a,node b){
    	return a.second<=b.second;
    }
    
    sort(arr+1,arr+n+1,cmp);
    
  • 步骤三:

    • 对每个服务时间和截止时间进行判定,\(now\) 来累加总的服务时间。若\(now\) 大于截止时间就更新 \(ans\) ,经过\(n\) 次迭代,\(ans\) 被更新为\(n\)个延迟中最大的那个延迟。
      for(int i=1;i<=n;i++){
      	cout<arr[i].second) ans=max(ans,now-arr[i].second);
      }
    
源码
#include
using namespace std;

const int maxn = 1e6+10;
struct node{
    int first,second;
    int id;
}arr[maxn];
bool cmp(node a,node b){
	return a.second<=b.second;
}
int main(){
	int n;cin>>n;
	for(int i=1;i<=n;i++){
		cin>>arr[i].first;
		arr[i].id=i;
	}
	for(int i=1;i<=n;i++) cin>>arr[i].second;
	sort(arr+1,arr+n+1,cmp);
	int now=0,ans=0;
	for(int i=1;i<=n;i++){
		cout<arr[i].second) ans=max(ans,now-arr[i].second);
	}cout<