算法设计与分析笔记

递归

数学映射与计算机函数的区别:

计算机函数的行为多次调用可能结果不一致但是没有随机性的数学函数不会。

函数式编程:对计算机函数添加额外限制排除副作用(变为纯函数)使得其与数学映射行为均具有确定性。
额外限制:

禁止引用全局变量
禁止调用有全局影响的外部函数
禁止cout

递归函数的数学定义:用不同规模的输入定义自身

string f(int n){
	if (n==0) return "a";
	if (n==1) return "b";
	return f(n-1) + f(n-2);
}

f(2)应该为"ba",而不是"ab"。字符串的加法没有交换律,这种设计可以使得两次相邻的输出以上一次输出作为前缀。

幂集与全排列:

集合 S S S幂集的生成算法 f f f

如果 S = ∅ S=\emptyset S= f ( S ) = { { } } f(S)=\{\{\}\} f(S)={{}}
否则 S = { x } ∪ S ′ S=\{x\}\cup S' S={x}S f ( S ) = f ( S ′ ) ∪ { { x } ∪ s ∣ s ∈ f ( S ′ ) } f(S)=f(S') \cup \{\{x\}\cup s|s\in f(S')\} f(S)=f(S){{x}ssf(S)}

def permutations(S):
    res = []
    if len(S)==1:
        return [S]
    else:
        x = S[0]
        remain = permutations(S[1:])
        for sub_permu in remain:
            for i in range(len(sub_permu)+1):
                permutation = sub_permu[:i] + [x] + sub_permu[i:]
                res.append(permutation)
        return res
res = permutations([0,1,2])
print(res)
#include <iostream>
#include <vector>
using namespace std;

template<typename T>
using Subsets = vector<vector<T>>;

template<typename T>
std::ostream& operator<<(std::ostream& os, const Subsets<T>& subsets) {
    os << "[";
    for (const auto& subset : subsets) {
        os << "[";
        for (const auto& element : subset) {
            os << element << " ";
        }
        os << "] ";
    }
    os << "]";
    return os;
}

template<typename T>
Subsets<T> permutations(vector<T> S) {
    Subsets<T> res;
    if (S.size() == 1) {
        res.push_back(S);
        return res;
    }
    T x = S.front();
    vector<T> S_(S.begin() + 1, S.end());
    Subsets<T> remain = permutations(S_);
    for (auto sub_permu : remain) {
        for (int i = 0; i < sub_permu.size()+1; i++) {
            vector<T> permutation = *new vector<T>();
            vector<T> half_left(sub_permu.begin(), sub_permu.begin()+i);
            vector<T> half_right(sub_permu.begin()+i, sub_permu.end());
            half_left.push_back(x);
            for (auto element : half_left) {
                permutation.push_back(element);
            }
            for (auto element : half_right) {
                permutation.push_back(element);
            }
            res.push_back(permutation);
        }
    }
    return res;
}

int main()
{
    Subsets<int>res = permutations<int>({ 0,1,2 });
    cout << res;
}

Hanoi塔

void hanoi(int n, char from, char to, char aux) {
	if (n == 1) {
		std::cout << from << "->" << to << std::endl;
		return;
	}
	hanoi(n - 1, from, aux, to);
	hanoi(1, from, to, aux);
	hanoi(n - 1, aux, to, from);
}
int main() {
	hanoi(3, 'A', 'C', 'B');
	return 0;
}

依赖每步直接cout给出总得结果的递归函数不是纯函数:

同一递推步中 f ( n ) = f ( n − 1 ) + f ( n − 2 ) f(n)=f(n-1)+f(n-2) f(n)=f(n1)+f(n2)在数学上具有交换律,可以任意先算其中一个但是该函数不行
子算法在数学上可以在时间维度多次调用而不影响回归,但是该函数不行。

一个问题

这种版本的Hanoi每步只能知道该移动哪个但是无法给出每步移动的是编号几的积木。

解决方案

纯函数的递归函数更易于人类理解,如果一个递归函数可以写成纯函数,则一定可以用迭代实现。

#include "Hanoi.h"
#include <vector>
#include <array>
#include <set>
using namespace std;

void hanoi(int n, char from, char to, char aux) {
	if (n == 1) {
		std::cout << from << "->" << to << std::endl;
		return;
	}
	hanoi(n - 1, from, aux, to);
	hanoi(1, from, to, aux);
	hanoi(n - 1, aux, to, from);
}
using State = array<vector<int>, 3>;
vector<State> hanoi_pure(State s, int n, int from, int to) {
	if (n == 1) {
		State final = s;
		int disk = final[from].back();
		final[from].pop_back();
		final[to].push_back(disk);
		vector<State> result;
		result.push_back(final);
		return result;
	}
	vector<State> steps0 = hanoi_pure(s, n - 1, from, 3 - from - to);
	State s1 = steps0.back();
	vector<State> steps1 = hanoi_pure(s1, 1, from, to);
	State s2 = steps1.back();
	vector<State> steps2 = hanoi_pure(s2, n - 1, 3 - from - to, to);
	vector<State> result = steps0;
	for (State s : steps1) {
		result.push_back(s);
	}
	for (State s : steps2) {
		result.push_back(s);
	}
	return result;
}

int main() {
	hanoi(3, 'A', 'C', 'B');
	State init;
	init[0] = { 1,2,3 };

	vector<State> res = hanoi_pure(init, 3, 0, 2);
	for (State s : res) {
		for (vector<int> pillar : s) {
			for (auto disk : pillar) {
				cout << disk;
			}
			cout << " ";
		}
		cout << endl;
	}
	return 0;
}

纯函数的好处

GPT:可预测性:相同输入相同输出、易于测试:不依赖外部状态也不产生副作用,方便单元测试、无副作用:避免意外交互与数据污染、引用透明:可安全地替换一个表达式为其返回值(或反之)而不改变程序行为、并发执行:无需担心竞态条件或数据同步、缓存和记忆化:既然输出只依赖输入,结果可以提前保存。代码优化:编译器检测到纯函数的结果未被使用则可以安全移除而不用担心有副作用。

对物理世界进行抽象:生命游戏

尾递归 Tail Recursion

递归调用在函数的最后一步且返回值不会被用于进行其它计算,尾递归一定可以迭代实现。

递归树

当我们试图使用非递归方法计算时,有两个方向可以进行:1. 从递归边界向上计算;2. 从目标向下计算,使用memorized与pending。
第一种方法在递推关系与规模的映射本身比较稀疏的时候会浪费很多计算资源。第二种方法需要不停的视察pending中的每一项是否可计算。当这种视察本身需要的成本较低时我们倾向于使用第二种方法。此外pending容器内元素的视察顺序很可能影响总体效率。在当我们以一定顺序使用第二种方法同时每次不清除memorized的时候,我们可以借助这种按需计算的方法节省一定的计算资源,不必每次重新计算,但是总体效率取决于被要求计算的顺序。
递归解构了数学对象、归纳定义构造了数学对象。

构造表达式树的意义

我们在构造的过程中本身就已经获得了他的值,实际上并不需要完成的存储整棵树的结构来计算总的值,但是一旦建立了这棵树,我们就可以不仅仅计算整体的值而且还可通过递归拆解获得他其它的性质,例如:去除不必要的括号、允许把k个数字+1,能得到的最大值。

算法

每次找这个表达式中的最后一个运算符号,然后递归构造子树

总结

栈是数学递归的一个自然实现但不是唯一实现。(递归证明不完备性定理)

贪心

给出一个优化问题,对于一个有限离散的搜索空间,如果空间较小,枚举空间的所有可能解 x x x与对应的 f ( x ) f(x) f(x),我们总能获得对应问题的最优解。机试中,如果测试数据弱,我们总能通过枚举获取一定分数。但是合格的测试数据应该是反枚举的。这种情况下我们需要构造精心的算法来求解。
贪心搜索为:有一个优化问题,以当前解 x i x_i xi出发,以一定准则探索相邻有限解 { x j ∣ ∣ x j − x i ∣ ≤ r , j ≠ i } \{x_j||x_j-x_i|\le r,j \ne i\} {xj∣∣xjxir,j=i},如果 f ( x j ) f(x_j) f(xj)从优化标准上优于 f ( x i ) f(x_i) f(xi),则迭代使用 x j x_j xj为当前最优解,当迭代停止时,输出对应解。即从任意解出发,沿着以贪心策略获得的下一个解所构成的路径的终点是最优解。
贪心的正确性需要问题满足:任何局部最优解为全局最优解
梯度下降法就是一种典型的贪心算法,但是搜索空间并不一定满足 “任何局部最优解都是全局最优解”
典型的贪心问题:有 n n n个数字,可能的解要求是他的全排列之一。这种情况下,如果认为输入规模为 n n n,则蛮力枚举算法的时间复杂度为 O ( n ! ) O(n!) O(n!),计算上不可接受。
这类问题通常被构造为依某种指标对数字进行排序的问题。即 更有序的解一定是更优的解。如果问题被证明具有这样的性质,则一定可以使用贪心算法解决这个问题。因为排序问题可以使用贪心算法(冒泡排序:不停的减少逆序对)求解。
更有序的解一定是更优的解

证明框架:
1.证明解空间中的 n n n个数字和一个构造出的严格偏序关系构成一个偏序集。
2.证明该偏序集进一步是一个全序集。
3.证明 f ( x ) < f ( y ) , inv ( x ) < inv ( y ) , ∀ x , y ∈ Ω = P ( A , n ) f(x)<f(y), \text{inv}(x)<\text{inv}(y), \forall x, y \in \Omega=\text{P}(A, n) f(x)<f(y),inv(x)<inv(y),x,yΩ=P(A,n)
这样原问题被化归到寻找逆序数最小的 x x x,而sortedness维度上的最小可以通过 O ( n log ⁡ ( n ) ) O(n\log(n)) O(nlog(n))的排序算法求解。
关系:A binary relation R R R over sets X X X and Y Y Y is a subset of X × Y X \times Y X×Y (笛卡尔积)
偏序partial order:同源二元关系homogeneous binary relation
 1.反身性reflexivity, a ≤ a a \le a aa
 2.反对称性antisymmetry,if a ≤ b a \le b ab and b ≤ a b \le a ba then a = b a=b a=b
 3.传递性transitivity,if $a \le $ and b ≤ c b \le c bc then a ≤ c a \le c ac
严格偏序:
 1.非反身性irreflexivity, ¬ ( a < a ) \neg ( a<a) ¬(a<a)
 2.不对称性asymmetry,if a < b a<b a<b then not b < a b<a b<a
 3.传递性transitivity,if a < b a<b a<b and b < c b<c b<c then a < c a<c a<c
偏序集: P = < X , ≤ > P=<X, \le > P=<X,≤>,not every pair of elements needs to be comparable
全序集: P = < X , ≤ > P=<X, \le > P=<X,≤> a ≤ b , ∀ a , b ∈ P a \le b, \forall a, b \in P ab,a,bP

排序问题可以使用贪心算法

证明:通过交换相邻元素减小逆序对来获得排序度最高的解的搜索可使用贪心算法。
首先定义交换元素时,解的相邻:从一个解 x k = x k 1 x k 2 ⋯ x k n x_k=x_{k_1} x_{k_2} \cdots x_{k_n} xk=xk1xk2xkn,通过交换一对相邻元素得到的其它解 x k ′ x_{k^\prime} xk被定义为解 x k x_k xk的相邻元素。
我们只需要证明局部最优解是全局最优解。以 P = < A , < > , A = 1 , 2 , ⋯   , n P=<A,<>,A={1,2,\cdots,n} P=<A,<>A=1,2,,n,<为自然数序为例。

首先全局最优解存在且唯一,且逆序数为0: < 1 , 2 , ⋯   , n > <1,2,\cdots,n> <1,2,,n>。假设存在另一个最优解, i i i号位为 j j j i ≠ j i \ne j i=j,若 i < j i < j i<j
当元素 i i i的位置 k ≥ i + 1 k \ge i+1 ki+1时,这构成了一个逆序对,矛盾。
当元素 i i i的位置 k ≤ i − 1 k \le i-1 ki1时,由于存在 i − 1 i-1 i1个元素小于元素 i i i,假设没有元素与元素 i i i构成逆序,则 k > i − 1 k>i-1 k>i1,矛盾。
i > j i > j i>j同理,唯一性得证。

此问题局部最优解为全局最优解:
若有一个排列 π \pi π任意交换相邻元素都增加逆序数,则由定义为局部最优解。而在此问题中这个局部最优排列 π \pi π的逆序数一定是0,

假设不是0,则存在至少一个逆序对(inversion), π ( i ) > π ( j ) , i < j \pi(i)>\pi(j),i<j π(i)>π(j),i<j,那么一定存在相邻元素 π ( k ) > π ( k + 1 ) , i ≤ k ≤ j − 1 \pi(k)>\pi(k+1),i \le k \le j-1 π(k)>π(k+1),ikj1

j = i + 1 j=i+1 j=i+1时证明成立,当 j > i + 1 j>i+1 j>i+1时,假设 π ( k ) < π ( k + 1 ) , ∀ i ≤ k ≤ j − 1 \pi(k)<\pi(k+1), \forall i \le k \le j-1 π(k)<π(k+1),ikj1,则由严格偏序的传递性, π ( i ) < π ( j ) , i < j \pi(i)<\pi(j),i<j π(i)<π(j),i<j与前提相悖,假设不成立。
而此时交换这个相邻元素可以获得更优的解, π \pi π不是局部最优解。因此局部最优解的逆序数一定是0。而逆序数为0的解为全局最优解。贪心算法在此可以获得正确的解。

典型问题:
搜索空间为全排列
1.拿物品, n n n个物品价值分别为 a i , 1 ≤ i ≤ n a_i,1 \le i \le n ai,1in,每次拿完以概率 p i , 0 ≤ i ≤ n p_i, 0 \le i \le n pi,0in结束, p i p_i pi仅与次数有关。
要使所拿物品价值期望最大,应该以怎样的顺序拾取。
f ( π k ) = 0 ⋅ p 0 + a k 1 ⋅ p 1 + ( a k 1 + a k 2 ) ⋅ p 2 + ⋯ + ( a k 1 + ⋯ + a k n ) ⋅ p n = a k 1 ⋅ ∑ 1 n p i + a k 2 ⋅ ∑ 2 n p i + ⋯ + a k n ⋅ p n \begin{align*} f(\pi_k)&=0 \cdot p_0+a_{k_1} \cdot p_1+(a_{k_1}+a_{k_2}) \cdot p_2+ \cdots +(a_{k_1}+\cdots +a_{k_n}) \cdot p_n \\ &=a_{k_1} \cdot \sum_1^n p_i+a_{k_2} \cdot \sum_2^n p_i+ \cdots +a_{k_n} \cdot p_n \end{align*} f(πk)=0p0+ak1p1+(ak1+ak2)p2++(ak1++akn)pn=ak11npi+ak22npi++aknpn
f ( π k ) ≤ f ( π k ′ ) ⇔ σ ( π k ) ≤ σ ( π k ′ ) f(\pi_k) \le f(\pi_k^{\prime}) \Leftrightarrow \sigma(\pi_k) \le \sigma(\pi_k^{\prime}) f(πk)f(πk)σ(πk)σ(πk)
由于相邻解只有一个相邻元素顺序相反:
只需证:
a k j ⋅ ∑ j n p i + a k j + 1 ⋅ ∑ j + 1 n p i ≤ a k j + 1 ⋅ ∑ j n p i + a k j ⋅ ∑ j + 1 n p i ⇔ σ ( π k ) ≤ σ ( π k ′ ) a_{k_j} \cdot \sum_j^n p_i +a_{k_{j+1}} \cdot \sum_{j+1}^n p_i \le a_{k_{j+1}} \cdot \sum_j^n p_i +a_{k_{j}} \cdot \sum_{j+1}^n p_i \Leftrightarrow \sigma(\pi_k) \le \sigma(\pi_k^{\prime}) akjjnpi+akj+1j+1npiakj+1jnpi+akjj+1npiσ(πk)σ(πk)
a k j ⋅ ∑ j n p i + a k j + 1 ⋅ ∑ j + 1 n p i ≤ a k j + 1 ⋅ ∑ j n p i + a k j ⋅ ∑ j + 1 n p i a k j ⋅ p j ≤ a k j + 1 ⋅ p j a k j ≤ a k j + 1 \begin{align*} a_{k_j} \cdot \sum_j^n p_i +a_{k_{j+1}} \cdot \sum_{j+1}^n p_i &\le a_{k_{j+1}} \cdot \sum_j^n p_i +a_{k_{j}} \cdot \sum_{j+1}^n p_i \\ a_{k_j} \cdot p_j &\le a_{k_{j+1}} \cdot p_j \\ a_{k_j} &\le a_{k_{j+1}} \end{align*} akjjnpi+akj+1j+1npiakjpjakjakj+1jnpi+akjj+1npiakj+1pjakj+1
σ ( π k ) ≤ σ ( π k ′ ) \sigma(\pi_k) \le \sigma(\pi_k^{\prime}) σ(πk)σ(πk),得证。
2.n个同学取水,取水时间分别为 t i , 1 ≤ i ≤ n t_i,1 \le i \le n ti,1in,怎样的取水方案平均等待时间最小。
3.拼数问题,n个正整数能排成的最大整数。
调整直到不能更优
搜索空间为部分排列(依据标准将元素全部排序后,根据附加条件不断向解中加入不矛盾的元素)
1.活动安排,有若干场活动,怎样安排能在给定时间内安排最多数量的活动。
证明:[exchange argument]
illinois
stanford

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值