【算法设计与分析】简介与基本概念

计算问题和算法

计算问题:输⼊和输出之间的⼆元关系,例如输入两个数,计算它们的和。每⼀个输⼊可能对应零个、⼀个或者多个输出。
算法:找到以上⼆元关系的⽅法。对于每⼀个输⼊,能在有限时间内正确找到其对应的输出。
算法的效率:一般通过时间复杂度来衡量。
算法的正确性:一般通过归纳法证明。

最⼤⼦数组问题

输入:一个长度为 n 的数组。
输出:该数组的最大的子数组
类似题目洛谷:最大子段和,注意此处讨论的最大子数组可以为空,而最大子段和中的不能为空

穷举法求最大子数组

穷举所有子数组a[i…j],计算子数组的和,得到最大的子数组。

int dynamic_programming(vector<int>& nums) {
    int ans=0;
    for(int i=0;i<nums.size();i++){
        int t=0;
        for(int j=i;j<nums.size();j++){
            t+=nums[j];
            ans=max(ans,t);
        }
    }
    return ans;
}

在最大子段和上提交的代码:

#include<bits/stdc++.h>
using namespace std;

int dynamic_programming(vector<int>& nums) {
    int ans=nums[0];
    for(int i=0;i<nums.size();i++){
        int t=0;
        for(int j=i;j<nums.size();j++){
            t+=nums[j];
            ans=max(ans,t);
        }
    }
    return ans;
}
int main() {
    int n, tmp;
    vector<int> v;
    cin>>n;
    while(n--){
        cin>>tmp;
        v.push_back(tmp);
    }
    cout<<brute_force(v)<<endl;
    return 0;
}

最终结果,因为数据量较大,二重循环会超时。因此枚举答案是行不通的,需要更优的算法。
在这里插入图片描述

归纳法求最⼤⼦数组

归纳:如何基于较小规模的问题来求解较⼤规模的问题

  • 基本情况:⾜够小的问题可以直接求解。例如将大规模问题不断划分,最终变为O(1)的问题,就可以直接求解,再合并。
  • 归纳步骤:建立小问题和⼤问题之间的关系(递归关系)。在得到子问题的解时,需要得到子问题与父问题的关系,用于将子问题合并,从而得到父问题的解。

问题:求a[i…j]的最大子数组

  • 基本情况:i==j时,此时最大子数组即为max{a[i], 0}(最大子数组可以为空)。
  • 递归关系: O P T ( a [ 1.. n ] ) = m a x O P T ( L e f t ) , O P T ( R i g h t ) , S ( C r o s s ) OPT(a[1..n]) = max{OPT(Left), OPT(Right), S(Cross)} OPT(a[1..n])=maxOPT(Left),OPT(Right),S(Cross)。S(Cross)是指横跨Left和Right的最大值。

均匀划分

归纳法将数组 a 分为Left和Right两部分,均匀划分则尽量将Left和Right的长度划分至相等。
此时:Left = a[1…m], Right = a[m+1…n], m=(1+n)/2;
Cross是横跨Left和Right的子数组,因此必须至少包含Left和Right的一个元素:

  • Cross = a[i…j] = a[i…m] + a[m+1…j]
  • a[i…m]:从a[m]开始,向左扫描数组
  • a[m+1…j]:从a[m+1]开始,向右扫描数组
    在这里插入图片描述
int divide_and_conquer(const vector<int>& a, int i, int j){
    if(i==j) return max(a[i],0);
    int m=(i+j)/2;
    int left = divide_and_conquer(a,i,m);
    int right = divide_and_conquer(a,m+1,j);
    int cross_l = a[m],mcl=a[m];
    for(int k=m-1;k>=i;k--){
        cross_l+=a[k];
        mcl=max(mcl, cross_l);
    }
    int cross_r = a[m+1],mcr=a[m+1];
    for(int k=m+2;k<=j;k++){
        cross_r+=a[k];
        mcr=max(mcr, cross_r);
    }
    return max(left, max(right, mcl+mcr));
}

在最大子段和上提交的代码:

#include<bits/stdc++.h>
using namespace std;

int divide_and_conquer(const vector<int>& a, int i, int j){
    if(i==j) return a[i];
    int m=(i+j)/2;
    int left = divide_and_conquer(a,i,m);
    int right = divide_and_conquer(a,m+1,j);
    int cross_l = a[m],mcl=a[m];
    for(int k=m-1;k>=i;k--){
        cross_l+=a[k];
        mcl=max(mcl, cross_l);
    }
    int cross_r = a[m+1],mcr=a[m+1];
    for(int k=m+2;k<=j;k++){
        cross_r+=a[k];
        mcr=max(mcr, cross_r);
    }
    return max(left, max(right, mcl+mcr));
}
int main() {
    int n, tmp;
    vector<int> v;
    cin>>n;
    while(n--){
        cin>>tmp;
        v.push_back(tmp);
    }
    cout<<divide_and_conquer(v,0,v.size()-1)<<endl;
    return 0;
}

在这里插入图片描述

不均匀划分

仍然将数组划分成两个部分,但尽量不均匀的划分,将数组分成长度尽量不等的两部分: a[1…n−1] a[n…n]

O P T ( a [ 1.. n ] ) = m a x ( O P T ( L e f t ) , O P T ( R i g h t ) , S ( C r o s s ) ) OPT(a[1..n]) = max(OPT(Left), OPT(Right), S(Cross)) OPT(a[1..n])=max(OPT(Left),OPT(Right),S(Cross))

int divide_and_conquer(const vector<int>& a, int i, int j){
    if(i==j) return max(a[i],0);
    int m=j-1;
    int left = divide_and_conquer(a,i,m);
    int right = divide_and_conquer(a,m+1,j);
    int cross_l = a[m],mcl=a[m];
    for(int k=m-1;k>=i;k--){
        cross_l+=a[k];
        mcl=max(mcl, cross_l);
    }
    return max(left, max(right, mcl+a[j]));
}

在最大子段和上提交的代码:

#include<bits/stdc++.h>
using namespace std;

int divide_and_conquer(const vector<int>& a, int i, int j){
    if(i==j) return a[i];
    int m=j-1;
    int left = divide_and_conquer(a,i,m);
    int right = divide_and_conquer(a,m+1,j);
    int cross_l = a[m],mcl=a[m];
    for(int k=m-1;k>=i;k--){
        cross_l+=a[k];
        mcl=max(mcl, cross_l);
    }
    return max(left, max(right, mcl+a[j]));
}
int main() {
    int n, tmp;
    vector<int> v;
    cin>>n;
    while(n--){
        cin>>tmp;
        v.push_back(tmp);
    }
    cout<<divide_and_conquer(v,0,v.size()-1)<<endl;
    return 0;
}

因为每次只减少一个元素,因此递归深度也有n,此时退化成了与二重循环相同的时间复杂度,因此后三个样例也超时了。
在这里插入图片描述

动态规划求最大子数组

对于数字 i 都有两个状态:

  • 如果选择 a[i] ,则从a[1…i-1]的最大子数组 + a[i],与a[i] (此时a[1…i-1]的最大的子数组会让a[i]变得更小)。 p r e i = m a x ( n i , p r e i − 1 + n i ) pre_i=max(n_i, pre_{i-1}+n_i) prei=max(ni,prei1+ni)
  • 如果不选 a[i], 此时则a[i]不存在与最长子数组中,子数组从a[i+1]开始。对应 p r e i + 1 = n i + 1 pre_{i+1}=n_{i+1} prei+1=ni+1

对于计算a[i]的 p r e i pre_i prei,此时a[i-1]一定是选择了的: p r e i − 1 = m a x ( n i − 1 , p r e i − 2 + n i − 2 ) pre_{i-1}=max(n_{i-1}, pre_{i-2}+n_{i-2}) prei1=max(ni1,prei2+ni2)
因此基于此,pre是a[1…i]的包含a[i]的最大的子数组的值,ans代表a[1…i]的最大的子数组的值。

int divide_and_conquer(const vector<int>& a){
    int ans=0, pre=0;
    for(int n : a){
        pre=max(n, pre+n);
        ans=max(ans,pre);
    }
    return ans;
}

在最大子段和题目中提交的代码:

#include<bits/stdc++.h>
using namespace std;

int divide_and_conquer(const vector<int>& a){
    int ans=a[0], t=0;
    for(int n : a){
        t=max(n, t+n);
        ans=max(ans,t);
    }
    return ans;
}
int main() {
    int n, tmp;
    vector<int> v;
    cin>>n;
    while(n--){
        cin>>tmp;
        v.push_back(tmp);
    }
    cout<<divide_and_conquer(v)<<endl;
    return 0;
}

在这里插入图片描述

算法正确性证明

归纳假设:对于某个整数 ,算法能够正确地计算:

  • L[k]:以a[k]结尾的⼦数组的最⼤和。
  • ans:数组a[1…k]的⼦数组的最⼤和。

基本情况:k=1,数组只有一个元素a[1],计算完毕,L[k]=a[1],ans = max(a[1], 0)。
归纳步骤:令a[k+1]结尾的最大子数组是a[i…k+1]

  • i = k+1或者i < k+1
  • 对于后⼀种情况,a[i…k]必然是以a[k]结尾的最⼤⼦数组,根据归纳假设,其解为L[k]。
  • 所以 max(a[k+1], L(k) + a[k+1]) 能够正确计算L(k+1)

因此能正确得到最大子数组。

算法效率分析

Word-RAM模型

内存:由⼀系列连续的内存单元组成,每个内存单元可以存放 个比特,且具有⼀个地址,地址范围:0 ~ 2^w-1

  • 内存单元也称为word

处理器

  • 可以在常数时间内读写⼀个内存单元
  • 可以在常数时间内对两个内存单元中的内容进⾏基本的⼆元运算:加、减、乘、除、模、位运算、逻辑运算

输⼊输出:按内存单元逐⼀读⼊或写出

算法性能

  • 时间:基本运算的数量
  • 空间:使用内存单元的数量

渐近记号

一般对于算法的效率,采用最坏情况来进行分析。
渐进记号一般指最坏情况下算法使用的基本操作的数量。
在分析基本操作数量时,渐近记号忽略:

  • 常数因⼦:仍然和机器、编程语⾔等因素相关
  • 低阶项:输⼊规模很⼤时⽆关紧要

5 n 2 + 4 n + 3 5n^2 + 4n + 3 5n2+4n+3 —> 5 n 2 5n^2 5n2 —> n 2 n^2 n2

O记号

O ( g ( n ) ) O(g(n)) O(g(n)) = { f ( n ) f(n) f(n):存在正常量c和 n 0 n_0 n0,使得对所有 n > = n 0 n>=n_0 n>=n0,有 0 < = f ( n ) < = c g ( n ) 0<=f(n)<=cg(n) 0<=f(n)<=cg(n)}。
f ( n ) = O ( g ( n ) ) f(n)=O(g(n)) f(n)=O(g(n))表示函数 f ( n ) f(n) f(n)属于函数集合 O ( g ( n ) ) O(g(n)) O(g(n))
在这里插入图片描述

O记号的性质

在这里插入图片描述
在这里插入图片描述

Ω记号

Ω ( g ( n ) ) Ω(g(n)) Ω(g(n)) = { f ( n ) f(n) f(n) :存在正常量c和 n 0 n_0 n0,使得对所有 n > = n 0 n>=n_0 n>=n0,有 0 < = c g ( n ) < = f ( n ) 0<=cg(n)<=f(n) 0<=cg(n)<=f(n)}
f ( n ) = Ω ( g ( n ) ) f(n)=Ω(g(n)) f(n)=Ω(g(n))表示函数 f ( n ) f(n) f(n)属于函数集合 Ω ( g ( n ) ) Ω(g(n)) Ω(g(n))

渐近记号与极限

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

等式的右边只有渐近记号,比如 n = O ( n 2 ) n = O(n^2) n=O(n2)

  • 等号实际上是集合的成员关系,即 n ∈ O ( n 2 ) n ∈ O(n^2) nO(n2)

等式的右边包含渐近记号,比如 2 n 2 + 3 n + 1 = 2 n 2 + Θ ( n ) 2n^2 + 3n + 1 = 2n2 + Θ(n) 2n2+3n+1=2n2+Θ(n)

  • Θ(n)代表⼀个匿名函数f(n),其中 f(n) ∈ Θ(n)
  • 用于隐藏⽆关紧要的细节

等式的左边包含渐近记号,比如 2 n 2 + Θ ( n ) = Θ ( n 2 ) 2n^2 + Θ(n) = Θ(n2) 2n2+Θ(n)=Θ(n2)

  • ⽆论怎样选择等号左边的匿名函数 f ( n ) ∈ Θ ( n ) f(n) ∈ Θ(n) f(n)Θ(n),总有⼀种办法来选择等
    号右边的匿名函数 g ( n ) ∈ Θ ( n 2 ) g(n) ∈ Θ(n2) g(n)Θ(n2),使得等式成立,即 2 n 2 + f ( n ) = g ( n ) 2n2 + f(n) = g(n) 2n2+f(n)=g(n)

求解最⼤⼦数组算法的效率

穷举法求最大子数组: T ( n ) = O ( n 2 ) T(n) = O(n^2) T(n)=O(n2)

归纳法求最⼤⼦数组,均匀划分: T ( n ) = 2 T ( n / 2 ) + O ( n ) T(n) = 2T(n/2) + O(n) T(n)=2T(n/2)+O(n) --> T ( n ) = O ( n l o g n ) T(n) = O(nlogn) T(n)=O(nlogn)

归纳法求最⼤⼦数组,不均匀划分: T ( n ) = T ( n − 1 ) + O ( n ) T(n) = T(n -1) + O(n) T(n)=T(n1)+O(n) --> T ( n ) = O ( n 2 ) T(n) = O(n^2) T(n)=O(n2)

归纳法求最⼤⼦数组,不均匀划分(cross优化): T ( n ) = T ( n − 1 ) + O ( 1 ) T(n) = T(n -1) + O(1) T(n)=T(n1)+O(1) --> T ( n ) = O ( n ) T(n) = O(n) T(n)=O(n)

int left_mid_max_recur(const vector<int>& a, int n){
	L[n] = (n == 0) ? a[0] : max(L[n - 1] + a[n], a[n]);
	return L[n]; // global variable L = vector<int>(a.size());
}

动态规划法求最大子数组: T ( n ) = O ( n ) T(n) = O(n) T(n)=O(n)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值