活动安排问题的动态规划、贪心算法和树搜索算法求解

本文探讨了活动规划问题,采用动态规划、贪心算法及树搜索三种方法求解最大相容活动集。通过实例分析,对比不同算法的特点与适用场景。

一、题目描述

1、题目

  给定一组活动的开始和结束时间,在其中选出最多活动,使它们不互相冲突。结果我们称之为最大相容活动集

2、题目分析

  其实这个问题还是比较容易理解的,就是在一定时间内尽可能让更多的活动被举行。
  比如有一个多媒体教室,现在有四个待举办活动A、B、C、D。A是在8:00到10:00举行,简单记为[8, 10];B是[12, 14];C是[15, 17];D是[11, 19]。为了让尽可能多的活动举行,很明显我们要选择A、B、C为最大相容活动集,因为D与B、C都冲突。

二、分析并解决问题

1、动态规划

  这里不再具体讲解动态规划的概念了,在我之前的博客里已经有了,如果不知道什么是动态规划的话,可以自行百度或者到我之前的博客简单了解一下:利用动态规划解决最长增长子序列问题

(1)分析优化解的结构

  在分析之前,我们需要先对活动按照活动结束时间升序排序,这有利于我们之后的分析。
  首先,假设我们有已经按照活动结束时间升序排序的活动集[A, B, C, D, E]。如果我们想求[A, B, C, D, E]的最大相容活动集,其实就是[A, B, C, D]的最大相容活动集与活动E的相结合得出的最大相容活动集。但这样说可能比较难以理解,所以我们换种说法,假设A’、B’、C’、D’分别是以A、B、C、D活动结尾的最大相容活动集的个数,所以[A, B, C, D, E]的最大相容活动集的活动个数就是A’、B’、C’、D’与A’’、B’’、C’’、D’’(如果A与E相容,则A’‘就是A’+1)中的最大值。
  同样的,要求[A, B, C, D]的最大相容活动集,那该活动集的活动个数便是A’、B’、C’与A’’、B’’、C’'中的最大值。以此类推。
  用比较规范的语言来说就是:
在这里插入图片描述

  其实,这种方法也可以用来解决最大上升子序列的问题,与我之前博客里的方法不一样,大家可以思考一下如何用这种方法解决最大上升子序列问题。

(2)构造状态转换方程

  首先,我们使用数组actiArr[i]来记录以下标为i的活动结尾的最大相容活动个数;用preActiArr[i]来记录最大相容活动集中下标为i的活动的前一个活动的下标
  状态转换方程为:
在这里插入图片描述

  当然这是最基本的转换方程,编程时我们可以添加别的变量使其变得更为简单。

(3)子问题重叠性

  由于每次计算预选活动集P时都要获取以之前活动为结尾的最大相容活动集,所以具有子问题重叠性。

2、贪心算法

  实际上,活动选择问题就是比较经典的可以用贪心算法解决的问题。
  同样的,在分析之前,我们需要先对活动按照活动结束时间升序排序,这有利于我们之后的分析。

(1)选择贪心策略

  这里的贪心策略非常简单,每次尽量选择结束时间最早的活动,这样就余下更多的时间,可能容纳更多的活动。

(2)优化子结构和贪心选择性的证明

  在这里就不证明了,其实思路和我之前的博客一样,要是感兴趣的话可以去利用贪心算法解决最小平铺路径问题看看。

3、树搜索算法

(1)树搜索算法简介与分析

  其实很多问题的解可以表示成为树。解为树的节点或路径。求解这些问题可以转化为树搜索问题。比如深度表示活动的下标,如果在该深度的节点是左子节点,则表示包含该活动;如果是右子节点,则表示不包含该活动。这里我们便用这个思路来解决该问题。
  我个人感觉树搜索算法并不是那么常用,比如这个活动规划问题使用树搜索算法就不是特别合适。但树搜索算法中还是有很多方法分支的,如果感兴趣可以学习一下,但在这里就不详细说明了。
  在这里我们要使用Hill Climbing策略(爬山策略:首先利用贪心思想快速求出一个较优解。)进行分支界限搜索(分支界限搜索:快速找到一个可行解,并将该解作为阈值,在其他节点进行搜索的时候,预测该节点的极值解(可能是最大解或是最小解,要看问题是要求求最大值或最小值),与阈值相比较,如果不满足阈值的话,直接不再向下搜索,因为我们已经知道再往下搜索得出的解一定不是最终解,再往下搜索也没有意义了,我们也可以称这种方式为枝剪)。
  同样的,在分析之前,我们需要先对活动按照活动结束时间升序排序,这有利于我们之后的分析。而树的深度表示活动的下标
  因为我们用了界限分支搜索,所以要确定一个预测解的函数。这里我们预测该节点向下搜索得出解(最大相容活动集)的个数 n 是(该活动之前选择的活动个数 + 该活动之后不与该活动冲突的活动个数)。如果 n 不大于爬山策略得出的解,该节点就不再向下搜索。
  当然树的节点也需要储存一些数据,这里我们设定节点的结构是:

1、parent -> 父节点;
2、leftChild -> 左子节点;
3、rightChild -> 右子节点;
4、border -> 当前边界值(即我们选择的活动集合中最晚结束时间);
5、extra -> 被选择活动的数量(通过这个预测最大可能活动数);
6、actiId -> 活动下标(用深度表示下标比较麻烦且不直观);

  我们选择爬山策略的的贪心策略就是尽可能构建左子节点(说白了和上面的贪心算法的贪心策略一样,尽可能选结束时间早的活动,所以通过爬山策略取得的解已经是最优解,之后的搜索操作都是无意义的,因为这个问题本来就不适合用树搜索算法做,这里只是模拟树搜索算法的过程)。

三、算法实现

1、c语言

  首先我们需要几个头文件声明一些结构体、随机生成活动序列的函数以及排序函数。
  活动节点(node.h):

#ifndef __NODE_H__
#define __NODE_H__

// node
typedef struct Node{
   
   
    int x;  // 活动开始时间
    int y;  // 活动结束时间
} Node;

#endif

  布尔类型(bool.h):

#ifndef __BOOL_H__
#define __BOOL_H__

// bool type
typedef enum {
   
   
    false, true
} bool;

#endif

  获取随机活动集合(random_arr.c):这里函数不是很严谨,可能存在(20, 20)的活动,但没什么影响。

#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#include "bool.h"
#include "node.h"

//#define SIZE 10  // 活动数

#define random(min, max) (((min) + rand()%((max)-(min)))+1)  // 随机数组上下界

//#define RANGE_MIN 0  // 下界

//#define RANGE_MAX 20  // 上界

void randomArr(Node* S, int SIZE, int RANGE_MIN, int RANGE_MAX){
   
   
    srand((unsigned) time(NULL));
    for(int i = 0; i < SIZE; i++){
   
   
        Node node;
        node.x = random(RANGE_MIN, RANGE_MAX);
        node.y = random(RANGE_MIN, RANGE_MAX);
        while(node.x >= node.y){
   
   
            if(node.x == RANGE_MAX){
   
   
                node.y = RANGE_MAX;
                break;
            }
            if(node.x == RANGE_MAX - 1){
   
   
                node.y = RANGE_MAX;
            } else {
   
   
                node.y = node.y + random(node.x - node.y + 1, RANGE_MAX - node.y);
            }
        }
        S[i] = node;
    }
}

  归并排序(merge_sort.c):

#include "bool.h"
#include "node.h"

// 归并排序(稳定的)
// x -> true : 对前界排序
// asc -> true : 升序排序
void mergeSort(Node* nodes, int size, bool x, bool asc){
   
   
    _mergeSort(nodes, 0, size - 1, x, asc);
}

// 归并排序
void _mergeSort(Node* nodes, int left, int right, bool x, bool asc){
   
   
    if(left < right){
   
   
        int mid = (left + right) / 2;
        _mergeSort(nodes, left, mid, x, asc);
        _mergeSort(nodes, mid + 1, right, x, asc);
        _merge(nodes, left, mid, right, x, asc);
    }
}

// 归并
void _merge(Node* nodes, int left, int mid, int right, bool x, bool asc){
   
   
    // 暂存结果
    Node temp[right - left + 1];
    // 左右索引
    int l_idx = left;
    int r_idx = mid + 1;
    int temp_idx 
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值