茶艺师学算法打卡18:贪心、分治、回溯

124 阅读4分钟

茶艺师学算法打卡18:贪心、分治、回溯

学习笔记

贪心、分治、回溯,他们是3种算法思想。
不是某种具体的代码框架,而是在某种特定背景问题下的思考方向。

贪心

简单来说,就是“在一堆限制里求最好”。
具体的做法就是“每一步都要最大”。
但有这么一句“在每一场战斗都获得胜利,不见得能赢下整个战争。”,在有些场合,贪心算法算来的反而不是最优解。
因此,在决定使用贪心算法时,还得试着证明“在这里使用没问题”

用一道题感受一下,ACW905. 区间选点

题目说明
给定 N 个闭区间 [ai,bi] ,请你在数轴上选择尽量少的点,使得每个区间内至少包含一个选出的点。

输出选择的点的最小数量。

位于区间端点上的点也算作区间内。

输入格式
第一行包含整数 N ,表示区间数。

接下来 N 行,每行包含两个整数 ai,bi ,表示一个区间的两个端点。

输出格式
输出一个整数,表示所需的点的最小数量。

数据范围
1N105,1≤N≤105,
109aibi109−109≤ai≤bi≤109

输入样例:

3
-1 1
2 4
3 5  

输出样例:

2

题目分析
在数轴上选点,尽可能覆盖更多的区间,现在求这个点数的最小值。
如图,很容易看出,用两点(蓝色)即可达到要求。

这里试试用贪心思想,把这些区间按照右端点排个序,然后选每个区间的右端点。
因为每个区间的右端点就是这个区间的“最远的地方”,看能不能够到别的区间里面。
够得着,就不用管;够不着,所需点的数量才加1。
如何证明该思路正确呢?
在数学上,要证明两个数 A=BA = B,可以使用这样的套路:

先证明AB,再证明AB先证明 A \leq B ,再证明 A \geq B

我们设通过贪心思想算出的答案是 cnt ,最优答案为 ans 。

  • 证明 anscntans \leq cnt : cnt 是可行答案的一种,而 ans 是所有可能答案的最小值,因此能得到 anscntans \leq cnt
  • 证明 anscntans \geq cnt : 在经过排序,所有区间都不没有交集的情况下,要每个区间都至少选右端点,即 cnt 才能满足要求,因此可以得到 anscntans \geq cnt

示例代码

const N = 100010

type Section struct {
    l, r int
}

var (
    in = bufio.NewReader(os.Stdin)
    s [N]*Section
    )
    
func main() {
    n := 0
    fmt.Fscan(in, &n)
    
    l, r := 0, 0
    for i := 0; i < n; i++ {
        fmt.Fscan(in, &l, &r)
        s[i] = &Section{l, r}
    }
    
    sort.Slice(s[0: n], func(i, j int) bool {return s[i].r < s[j].r})
    
    res, p := 1, s[0].r 
    for i :=1; i < n; i++ {
        if s[i].l > p {
            res++
            p = s[i].r
        }
    }
    
    fmt.Print(res)
}

分治

简单来说,就是把一个问题“分而治之,最后再把结果合并一起”。
像是快速排序、归并排序,其背后原理就是分治。
这里直接使用 785. 快速排序 来感受一下。
题目说明
给定你一个长度为 n 的整数数列。

请你使用快速排序对这个数列按照从小到大进行排序。

并将排好序的数列按顺序输出。

输入格式
输入共两行,第一行包含整数 n 。

第二行包含 n 个整数(所有整数均在 1∼109 范围内),表示整个数列。

输出格式
输出共一行,包含 n 个整数,表示排好序的数列。

数据范围
1n1000001≤n≤100000
输入样例:

5
3 1 2 4 5

输出样例:

1 2 3 4 5

题目分析
基于分治思想的排序
挑选数组中的一个值x,把其他数组大于x放右边,小于x的放左边
接着递归处理左半段和右半段

把其他数组大于x放右边,小于x的放左边
其暴力做法是把小于x放进数组A, 大于x的放进数组B,然后AB拼起来。
为了减少扫描数组的次数与节省数组空间,有这优雅做法:
用左右指针扫描数组,左指针扫到第一个大于等于x的数,停下;右指针扫到第一个小于等于x的数,停下;然后被指针指着的数交换。

示例代码

const N int = 1e5 + 10

var (
    out = bufio.NewWriter(os.Stdout)
    in = bufio.NewReader(os.Stdin)
    n int 
    nums [N]int
    )
    
func quickSort(l, r int) {
    if l >= r {return}
    i, j, x := l - 1, r + 1, nums[l + (r - l) >> 1]
    for i < j {
        for {
            i++
            if nums[i] >= x {break}
        }
        for {
            j--
            if nums[j] <= x {break}
        }
        if i < j {
            nums[i], nums[j] = nums[j], nums[i]
        }
    }
    quickSort(l, j)
    quickSort(j + 1, r)
}

func main() {
    defer out.Flush()
    
    fmt.Fscan(in, &n)
    for i := 0; i < n; i++ {
        fmt.Fscan(in, &nums[i])
    }
    
    quickSort(0, n - 1)
    
    for i := 0; i < n; i++ {
        fmt.Fprintf(out, "%d ", nums[i])
    }
}

回溯

就是递归的实现之一,不用太纠结其定义。
好好练练递归相关的题目就好了。