茶艺师学算法打卡18:贪心、分治、回溯
学习笔记
贪心、分治、回溯,他们是3种算法思想。
不是某种具体的代码框架,而是在某种特定背景问题下的思考方向。
贪心
简单来说,就是“在一堆限制里求最好”。
具体的做法就是“每一步都要最大”。
但有这么一句“在每一场战斗都获得胜利,不见得能赢下整个战争。”,在有些场合,贪心算法算来的反而不是最优解。
因此,在决定使用贪心算法时,还得试着证明“在这里使用没问题”
用一道题感受一下,ACW905. 区间选点
题目说明
给定 N
个闭区间 [ai,bi]
,请你在数轴上选择尽量少的点,使得每个区间内至少包含一个选出的点。
输出选择的点的最小数量。
位于区间端点上的点也算作区间内。
输入格式
第一行包含整数 N
,表示区间数。
接下来 N 行,每行包含两个整数 ai,bi ,表示一个区间的两个端点。
输出格式
输出一个整数,表示所需的点的最小数量。
数据范围
输入样例:
3
-1 1
2 4
3 5
输出样例:
2
题目分析
在数轴上选点,尽可能覆盖更多的区间,现在求这个点数的最小值。
如图,很容易看出,用两点(蓝色)即可达到要求。
这里试试用贪心思想,把这些区间按照右端点排个序,然后选每个区间的右端点。
因为每个区间的右端点就是这个区间的“最远的地方”,看能不能够到别的区间里面。
够得着,就不用管;够不着,所需点的数量才加1。
如何证明该思路正确呢?
在数学上,要证明两个数 ,可以使用这样的套路:
我们设通过贪心思想算出的答案是 cnt ,最优答案为 ans 。
- 证明 : cnt 是可行答案的一种,而 ans 是所有可能答案的最小值,因此能得到
- 证明 : 在经过排序,所有区间都不没有交集的情况下,要每个区间都至少选右端点,即 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
个整数,表示排好序的数列。
数据范围
输入样例:
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])
}
}
回溯
就是递归的实现之一,不用太纠结其定义。
好好练练递归相关的题目就好了。