训练营第四周 分治法 贪心法

本文章由网安三兄弟合作编写。


分治法

分治法顾名思义就是分而治之,将大问题划分成一些规模较小的子问题,从而各个击破。
使用分治法题目的两大特征:
①平衡子问题:子问题的规模大致相同,能把问题分成两个规模相等的子问题。
②独立子问题:子问题之间相互独立。这是区别于动态规划算法的基本特征,在动态规划算法中子问题是相互联系的。
分治法能大大优化算法的复杂度,一般情况下能把O(n)的复杂度优化到O(log2n).
分治法建立模型的解题步骤分为:分解,解决,合并三步。
代码模板(参考y总)

1.快速排序

void quick_sort(int q[],int l,int r){
    if(l>=r) return;
    int x=q[l],i=l-1,j=r+1;
    while(i<j){
        do i++;while(q[i]<x);
        do j--;while(q[j]>x);
        if(i<j) swap(q[i],q[j]);
    }
    quick_sort(q,l,j);
    quick_sort(q,j+1,r);
}

2.归并排序

void merge_sort(int a[],int l,int r){
    if(l>=r) return;
    int mid=l+r>>1;
    merge_sort(a,l,mid),merge_sort(a,mid+1,r);
    int k=0,i=1,j=mid+1;
    while(i<=mid&&j<=r){
        if(a[i]<=a[j]) tmp[k++]=a[i++];
        else tmp[k++]=a[j++];
    }
    while(j<=mid) tmp[k++]=a[i++];
    while(j<r) tmp[k++]=a[j++];
    for(i=1,j=0;i<=r;i++,j++) a[i]=tmp[j];
}

排序问题是竞赛中的常用功能,一般可直接使用stl中的sort函数,
sort(a+1,a+1+n);//从小到大对a[1]到a[n]排序。
下面给出两道简单例题:

快速排序

#include<iostream>
using namespace std;
const int N = 100001;
int q[N];
void quick_sort(int q[], int l, int r) {
	if (l >= r) return;
	int x = q[(l+r)/2], i = l - 1, j = r + 1;
	while (i < j) {
		do i++; while (q[i] < x);
		do j--; while (q[j] > x);
		if (i < j) swap(q[i], q[j]);
	}
	quick_sort(q, l, j);
	quick_sort(q, j + 1, r);
}
int main() {
    int n;
	scanf("%d",&n);
	for (int i = 0; i < n; i++) scanf("%d",&q[i]);
	quick_sort(q, 0, n - 1);
	for (int i = 0; i < n; i++) printf("%d ",q[i]);
	return 0;
}

归并排序

#include<iostream>
using namespace std;
const int N = 1000001;
int n;
int a[N], tmp[N];
void merge_sort(int a[], int l, int r) {
	if (l >= r) return;
	int mid = l + r >> 1;
	merge_sort(a, l, mid), merge_sort(a, mid + 1, r);
	int k = 0, i = l, j = mid + 1;
	while (i <= mid && j <= r) {
		if (a[i] <= a[j]) tmp[k++] = a[i++];
		else tmp[k++] = a[j++];
	}
	while (i <= mid) tmp[k++] = a[i++];
	while (j <= r) tmp[k++] = a[j++];
	for (i = l, j = 0; i <= r; i++, j++) a[i] = tmp[j];
}
int main() {
	scanf("%d", &n);
	for (int i = 0; i < n; i++) scanf("%d", &a[i]);
	merge_sort(a, 0, n - 1);
	for (int i = 0; i < n; i++) printf("%d ", a[i]);
	return 0;
}
参考练习题

hdu 1425,求前k大的数;
poj 2388,求中间数;


贪心算法

概述

贪心是最容易理解的算法思想,把整个问题分解成多个步骤,在每个步骤都选取当前步骤的最优方案,直到所有步骤结束。即每次都使用短视判断做出局部最优解的选择,循环计算局部最优解的操作,直至出现计算结束。

贪心算法是一种算法思想,这种通过反复计算局部最优解以得到全局最优解的思路,给人的感觉并不靠谱——因为局部最优解并不一定是全局最优解(例如局部的极大值并不一定等于全局的最大值),而事实也确实如此——并不是所有的问题都能通过贪心算法获得最优解。

下面介绍的是两个经典的贪心问题——Huffman编码、模拟退火。

Huffman编码

Huffman编码(哈夫曼编码)是贪心思想的典型应用。

Huffman编码是一种可变长编码,它是一种依据字符串中每个字符的出现概率构造出平均编码长度最短的编码方式。

例如,有a,b,c三个字符,其中a出现的次数最多,那么我们可以用单个二进制位表示a(例如表示为0),用相对较长的二进制位表示出现频率较低的字符b,c(例如可以表示为1011)。

Huffman算法符合贪心算法的“最优子结构性质”和“贪心选择性质”,是利用贪心思想构造二叉编码树的算法。

例题

输入一个字符串,分别用普通ASCII编码(每个字符8bit)和Huffman编码,输出编码后的长度,并输出压缩比。

输入样例

AAAAABCD

输出样例

64 13 4.9

import heapq
def ASCII(S): # 计算ASCII编码长度
    return 8 * len(S)
def Huffman(S):
    cnt = 1
    T = [i for i in S]
    T.sort(key=lambda x: ord(x))
    A = []
    heapq.heapify(A)
    for i in range(1,len(S)):	# 统计出现每个字符出现次数并送入堆
        if T[i] != T[i - 1]:
            heapq.heappush(A,cnt)
            cnt = 1
        else:
            cnt += 1
    if cnt: heapq.heappush(A,cnt)
    res = 0
    while len(A) > 1:
        x = heapq.heappop(A)
        y = heapq.heappop(A)
        heapq.heappush(A,x + y)
        res += x + y
    return res

S = input()
a = ASCII(S)
b = Huffman(S)
print(a,b,round(a / b,1))
为什么使用每个字符的出现次数,就能计算出Huffman编码的长度呢?

我们注意到,Huffman函数中,使用小顶堆作为存储每个字符出现次数的容器。每一次从堆中取出的两个元素x,y,便是这组数中的两个最小的数。在每一次循环中,我们将x,y相加后再次放入堆中,同时会讲x,y计入res中。借助Huffman树做理解,每个数都分别处于叶子的位置,x,y作为最小的值(代表着某字符出现的最小次数),存储的深度最深,被加的次数无疑应该是最多的(相比起将更大的树放在树的最深处,使用最小的两个值,计算的结果,也即是编码的长度也将将会更小)。


模拟退火

为了更好地理解模拟退火算法,我们可以先了解一下“爬山算法”。

爬山算法是一种完全基于贪心思想的算法。这个算法会在当期的接临近的解空间中选择一个最优解作为当前解,知道达到一个局部最优解。

爬山算法是一个“地道的”贪心算法,因此它往往也有着贪心所具有的缺陷——利用爬山算法所求得的局部最优解并不一定等于全局最优解(如将爬山算法运用于一个连续函数上,它所求出的最值可能只是局部的一个极值)。

而模拟退火算法,则是贪心思想与概率的结合,它通过对概率的巧妙运用,达到跳过局部最优解并最终找到全局最优解的目的。

模拟退火的步骤
  1. 设置一个初始的温度T
  2. 温度下降,状态转移。从当前温度按降温系数下降到下一个温度,在新的温度计算当前状态。
  3. 如果温度降到设定的温度下界,程序停止。
例题

函数 F ( x ) = 6 x 7 + 8 x 6 + 7 x 3 + 5 x 2 − y x F(x) = 6x^7+8x^6+7x^3+5x^2-yx F(x)=6x7+8x6+7x3+5x2yx​​,其中x的范围是 0 ≤ x ≤ 100 0≤x≤100 0x100​。

输入y值,输出F(x)的最小值。

分析

因为F(x)是一个多项式函数,而多项式函数具有连续性,所以我们可以考虑通过贪心法求得最值。

但有一个问题会对传统的贪心法产生严重的影响——F(x)的图像具有多个峰,用——爬山法(传统的贪心法)计算出来的可能只是一个局部最大值,而非全局最大值。而能够以一定的概率跳过局部最优解从而找到全局最优解的模拟退火算法,则显现出了它在这类问题上独特的优势。

代码

import random
y = float(input())

def F(x):
    global y
    return 6 * x ** 7 + 8 * x ** 6 + 7 * x ** 3 + 5 * x ** 2 - y * x

def solve():
    T = 100	# 初始温度
    delta = 0.98	# 降温系数
    eps = 1e-8		# 终止温度
    flag = (1,-1)	# 正负号标识
    x = 50			# x,初始时取[0,100]的中点
    newx = 50		# 存储以一定概率接受的新x
    ans = F(x)		# 存最小值

    while T > eps:
        newx = x + flag[random.randint(0,1)] * T # 关键代码
        if newx >= 0 and newx <= 100:
            res = F(newx)
            if res < ans:
                ans = res
                x = newx
        T *= delta
    return ans

print(solve())
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

wingaso

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值