算法学习(五)

一、线段树
线段树是一种特殊的数据结构,常用于解决区间最值问题
RMQ(Range Minimum/Maximum Query)。假设有这样一个问题:给定一
个长度为 n 的数列,我们需要进行如下操作:
(1).求出区间最值
(2).元素的替换与修改
(3).给定区间求和问题
这个问题容易想到的常规方法是从头到尾遍历,复杂度为 O(n),
若进行 m 次修改或者求和,总的复杂度为 O(mn),当 mn 较大时,这个
算法并不太简单。因此,我们引入一种新的数据结构——线段树。它
对于这个问题,可以再 O(m log n)的时间内解决。
线段树是一种用于取件处理的数据结构,用二叉树实现。树的每
个节点代表一条线段[L,R],L 是左子节点,R 是右子节点,通常规定,
L<R,说明这个节点代表的不止一个点,左儿子为区间[L.,M],右儿子
代表的区间为[M+1,R],其中,M 为 L,R 的均值。因为运用到了二叉
树的结果,因此一般最大 log n 层就能达到底层,相当于二叉树的折
半查找,大大提高了查找和修改的效率。
下面通过一个例子引出线段树的点修改问题。
例 1. poj 2182 “Lost Cows”
有编号 1 到 n 的 n 头牛,2≤n≤8000,乱序排列,顺序未知。对 于每个位置的数字,都知道排在它前面比它小的数字有多少个。求乱
序数列的顺序。
这个问题我们可以用暴力法从后往前推,当然这个算法的问题在
于每次处理一个数字之后,都需要把剩下的牛重新排名,复杂度是
O(n^2)。下面我们用线段树实现这个题目的解法:
题目的思路与暴力法一样,从后往前推,区别在于线段树本身有
序的结构可以高效地对剩下的数字重新排名。
代码实现如下:
#include <iostream>
#include <stdio.h>
using namespace std;
const int Max = 10000;
struct {
	int l,r,len;
} tree[4*Max];
int pre[Max], ans[Max];

void BuildTree (int left, int right, int u) {
	tree[u].l = left;
	tree[u].r = right;
	tree[u].len = right - left + 1;
	if (left == right)
		return;
	BuildTree(left, (left + right) >> 1, u << 1);
	BuildTree(((left + right) >> +1) + 1, right, (u << 1) + 1);
}

int query(int u, int num) {
	tree[u].len--;
	if (tree[u].l == tree[u].r)
		return tree[u].l;
	if (tree[u << 1].len < num)
		return query((u << 1) + 1, num - tree[u << 1].len);
	if (tree[u << 1].len >= num)
		return query(u << 1, num);
}

int main() {
	int n, i;
	scanf_s("%d", &n);
	pre[1] = 0;
	for (int i = 2; i <= n; i++) 
		scanf_s("%d", &pre[i]);
	BuildTree(1, n, 1);
	for (int i = n; i >= 1; i--)
		ans[i] = query(1, pre[i] + 1);
	for (int i = 1; i <= n; i++)
		printf("%d\n", ans[i]);
	return 0;
}
当然这个题用完全二叉树实现线段树会更优。
 
二、贪心算法
贪心算法顾名思义,就是把一个问题分解成多个步骤,在每个步
骤时选取当前的最优方案,直到所有步骤结束。因为在执行当前策略
的时候并不考虑对之后的影响,所以,贪心算法存在一定的问题:即
局部的最优解叠加一定是全局的最优解吗?使用用贪心算法需要满
足以下特征:
(1).最优子结构性质
即一个问题的最优解包含其子问题的最优解的时候,称为最优子
结构性质。这就是说,局部最优解可以扩展到全局最优解问题。
(2).贪心选择性质
即问题的整体最优解是可以通过一系列局部最优的选择来得到。
值得注意的是,贪心算法在大多时候并不一定能够得到全局的最
优解,但它可以得到一个近似的可行解。
下面通过 Huffman 编码来说明贪心算法的应用。Huffman 编码是
前缀最优编码。
我们知道,字符串以二进制码来表示。当然,我们可以给 26 个英
文字母按顺序对应一个二进制编码,但是这样其实占用了不必要
的内存空间。因为每个字母出现的频次是不一样的。因此,我们 可以把出现次数多的字符用短码表示,出现次数少的字母用长码
表示,这样就就节约了字节长度。需要注意的是,编码算法要求
对于编码后的二进制串能够被唯一的还原,即某个编码是另一个
编码的前缀,这就是说,每个编码不能存在包含关系。Huffman
编码就是按照频次的高低,按出现频次从底层到顶层生成二叉树,
注意每一步都要按频次重新排序,以保证频次较少的字符被放在
底层。下图是哈夫曼算法的图解:
可以证明,哈夫曼算法满足贪心算法的要求,编码结果是最优解。
 
三、模拟退火算法
由上面的贪心算法我们可以知道,贪心算法得到的可能只是局部 的最优解,因为它做出这一步骤时并没有考虑后续的影响。因此,我
们采用一种新的策略:即退火算法。它基于这样一个物理原理:一个
高温物体降到常温,温度越高时的降温越快。模拟退火算法基于这一
思想,进行搜索,直到得到一个可行解。
这个问题可以比作一个爬山问题,如图所示,即 A 是此时的局部
最高点,B 是全局最高点,按照一般的贪心算法,程序只会滞留在 A
点。而模拟算法允许随机选择下一个状态:那么当新状态较旧状态更
优时,接受新状态;若新状态差于原状态,那么在一定的概率下接受
该状态,但这个概率应该随着时间逐渐降低。
模拟退火算法利用了 Boltzman 概率分布,当遇到不好的结果时,
算法不会立刻否决掉,而是会以一定概率接受这个解(状态转移)。这
一过程被称为 Metropolis 准则:
例 2. F(x) = 6x^7+8x^6+7x^3+5x^2-yx,其中 0≤x≤100,输入 y
值,输出 F(x)的最小值
#include<algorithm>
#include<iostream>
#include<cstdlib>
#include<cstdio>
#include<cmath>
#include<queue>
#include<stack>
#include<map>
#include<set>
#define db double
#define eps 1e-8
using namespace std;
db y, ans = 1e13;
int cas, f[2] = { 1,-1 };
db cal(db x) { return 6 * pow(x, 7.0) + 8 * pow(x, 6.0) + 7 * pow(x, 3.0) + 5 * pow(x, 2.0) - y * x; }
db solve(db temp)
{
	db ret = 1e13, nowd;
	db sx = 50.0, dc = 0.98;
	nowd = cal(sx);
	ret = min(ret, nowd);
	while (temp > eps)
	{
		db nx = sx + f[rand() % 2] * temp;
		if (nx > 100.0 || nx < 0.0);
		else
		{
			db nxd = cal(nx);
			ret = min(ret, nxd);
			if (nowd - nxd > eps)sx = nx, nowd = nxd;
		}temp *= dc;
	}return ret;
}
int main()
{
	scanf_s("%d", &cas);
	while (cas--)
	{
		scanf_s("%lf", &y);
		ans = solve(100);
		printf("%.4lf\n", ans);
	}
	return 0;
}
当然,这个程序还存在一定的缺点。因为它得到的是一个可行解
而非精确解,它的效率跟要求精度有关。除此之外,对随机扰动,终
止条件,衰减函数的设计对于该算法也非常重要。
 
 
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值