算法设计与分析期末复习

算法设计与分析期末复习 By Persona_owl

第一章 算法设计基础

算法:对特定问题求解步骤的一种描述,是为解决一个或一类问题给出的一个确定的有限长的操作序列。

程序 =算法+数据结构

算法的基本特点

  1. 输入:0 个或多个
  2. 输出:一个或多个
  3. 确定性:算法必须是没有歧义的
  4. 有穷性:算法必须在有限个计算步骤后终止(不能无限循环)
  5. 可行性:算法描述的操作可以通过已经实现的基本操作、执行有限次完成

有效算法的特征

  1. 正确性:能满足具体问题的需求,对于任何合法的输入,算法都能得到正确的结果
  2. 健壮性:对非法输入的抵抗能力,即对于错误输入,算法能识别并做出相应处理,而不是产生错误结果或陷入瘫痪
  3. 可读性:容易理解和实现
  4. 时间效率高:运行时间短
  5. 空间效率高:占用的存储空间尽量少

算法的描述方法

  1. 自然语言
  2. 程序流程图
  3. 伪代码--算法语言

算法设计的一般过程

数学建模 -> 算法设计 -> 正确性证明 -> 算法分析 -> 算法实现

第二章 算法分析基础

时间复杂度分析

  • 事后实验统计法
    编写算法对应程序,统计其执行时间。
  • 事前分析估算法(渐近分析法)
    渐近分析是指忽略具体机器、编程语言和编译器的影响,只关注在输入规模增大时,算法运行时间的增长趋势。
    • 时间复杂度由嵌套最深层语句(基本语句)的执行频度决定。
    • “最好情况”和“最坏情况”分别指执行的最少次数和最大次数。

2.2 渐近复杂度分析

  • 渐近分析主要关注当输入规模 n 趋近无穷大时,算法运行时间的增长趋势。
  • 通常只考虑最坏情况运行时间,因为它能给出任何输入的运行时间上界。

2.3 渐近符号

大O符号——渐进上界记号

如果存在正的常数 \(c\)\(n_0\),对于任意 \(n \geq n_0\),都有

\[T(n) \leq c \cdot g(n), \]

则记为

\[T(n) = O(g(n)), \]

\(g(n)\)\(T(n)\) 的上界。当多项式求和时,时间复杂度取最高阶项。

image-20250601111814817

大Ω符号——渐进下界记号

如果存在正的常数 \(c\)\(n_0\),对于任意 \(n \geq n_0\),都有

\[T(n) \geq c \cdot g(n), \]

则记为

\[T(n) = \Omega(g(n)), \]

\(g(n)\)\(T(n)\) 的下界。下界的阶越高,评估越准确。

image-20250601111920323

大Θ符号——渐进紧界记号

如果存在正的常数 \(c_1\), \(c_2\)\(n_0\),对于任意 \(n \geq n_0\),都有

\[c_2 \cdot g(n) \leq T(n) \leq c_1 \cdot g(n), \]

则称

\[T(n) = \Theta(g(n)), \]

\(g(n)\)\(T(n)\) 同阶。

image-20250601111957987

常见复杂度等级顺序

\[O(1) < O(\log n) < O(n) < O(n \log n) < O(n^{2}) < O(n^{3}) < O(2^{n}) < O(n!) \]

2.5 递归算法分析

迭代法

从初始递归方程开始,反复将右边的等式代入左边函数,直到出现初值。

image-20250601115832298

代入法

  1. 猜测解的形式(如 \(T(n) = a n \log n + b\) 等)
  2. 用数学归纳法求解其中常数
  3. 验证猜测正确性

递归树法

  1. 将递归方程展开为一棵带权重的树
  2. 每个节点表示一次子问题的代价
  3. 计算每层代价之和
  4. 将所有层之和累加得到总代价

image-20250601145848432

为了方便计算,假定每一层都满n

主方法(Master Theorem)

适用于求解形如:

\[T(n) = a T\left(\frac{n}{b}\right) + f(n) \]

其中:

  • \(a \geq 1\)
  • \(b > 1\)
  • \(f(n)\) 为渐近正函数

image-20250601120953889

2.6 渐近空间复杂度分析

空间复杂度关注算法运行过程中临时占用的存储空间大小,记作:

\[S(n) = O(g(n)) \]

含义和时间复杂度的渐近符号一致。

image-20250601121833861

第三章 分治法

3.1 分治法概述

将一个难以直接求解的大问题,分解成若干规模较小的子问题,递归地求解这些子问题,然后合并子问题的解得到原问题的解。

分治法三大特征

  1. 子问题和原问题性质相同
  2. 子问题之间相互独立
  3. 子问题规模缩小到一定程度时,可以直接求解

image-20250601154538978

3.2 求解排序问题

3.2.1 快排

快速排序基本思想:

  1. 在待排序的 n 个元素中任选一个元素(通常是第一个或中间位置)作为基准。
  2. 将数组划分为“小于基准”和“大于基准”两部分,并将基准放在中间。
  3. 对两部分子序列分别递归地进行快速排序,直到所有子序列长度为 0 或 1。
// 分区函数
int partition(int arr[], int low, int high)
{
    int pivot = arr[low]; // 选择第一个元素作为基准
    int left = low + 1;
    int right = high;

    while (true)
    {
        // 找到左侧第一个大于基准的元素
        while (left <= right && arr[left] <= pivot)
        {
            left++;
        }
        // 找到右侧第一个小于基准的元素
        while (left <= right && arr[right] >= pivot)
        {
            right--;
        }
        // 如果左右指针交错,退出循环
        if (left > right)
        {
            break;
        }
        // 交换左侧和右侧的元素
        swap(arr[left], arr[right]);
    }
    // 将基准放到正确的位置
    swap(arr[low], arr[right]);
    return right;
}

//快速排序
void quickSort(int arr[], int low, int high)
{
    if (low < high)
    {
        // pi是分区索引,arr[pi]已经排好序
        int pi = partition(arr, low, high);

        // 分别对左右子数组进行快速排序
        quickSort(arr, low, pi - 1);
        quickSort(arr, pi + 1, high);
    }
}

3.2.2 归并排序

归并排序核心思想:将一个序列递归地拆分为左右两个子序列,对子序列分别排序后,再进行“合并”操作。

// 合并两个子数组
void merge(int arr[], int left, int mid, int right)
{
    int n1 = mid - left + 1;
    int n2 = right - mid;

    // 创建临时数组
    int L[n1], R[n2];

    // 拷贝数据到临时数组
    for (int i = 0; i < n1; ++i)
        L[i] = arr[left + i];
    for (int j = 0; j < n2; ++j)
        R[j] = arr[mid + 1 + j];

    // 合并临时数组到原数组
    int i = 0, j = 0, k = left;
    while (i < n1 && j < n2)
    {
        if (L[i] <= R[j])
        {
            arr[k] = L[i];
            ++i;
        }
        else
        {
            arr[k] = R[j];
            ++j;
        }
        ++k;
    }

    // 拷贝L[]剩余元素
    while (i < n1)
    {
        arr[k] = L[i];
        ++i;
        ++k;
    }

    // 拷贝R[]剩余元素
    while (j < n2)
    {
        arr[k] = R[j];
        ++j;
        ++k;
    }
}

// 归并排序函数
void mergeSort(int arr[], int left, int right)
{
    if (left < right)
    {
        int mid = left + (right - left) / 2;

        // 递归排序左半部分
        mergeSort(arr, left, mid);
        // 递归排序右半部分
        mergeSort(arr, mid + 1, right);

        // 合并已排序的部分
        merge(arr, left, mid, right);
    }
}

3.3 求解查找问题

3.3.1 二分查找

//迭代写法
int binary_search(vector<int> &a,int x){
    int n=a.size();
    int l=1,r=n;
    while(l<=r){
        int mid=(l+r)>>1;
        int t=a[mid];
        if(t==x) return mid;
        else if(t<x){
            l=mid+1;
        }
        else if(t>x){
            r=mid-1;
        }
    }
    return -1;
}

//递归写法
int binary_search(vector<int> &a,int x,int l,int r){
    if(l>r) return -1;
    int mid=(l+r)>>1;
    int t=a[mid];
    if(t==x) return mid;
    else if(t<x){
        return binary_search(a,x,mid+1,r);
    }
    else if(t>x){
        return binary_search(a,x,l,mid-1);
    }
}

3.3.2 查找第 k 小元素

(动态维护第k小元素可以用对顶堆)

利用快速排序的“划分”思想(也称作“选择算法”):

  1. 从数组 a[l..r] 中任选一个元素作为 pivot,将数组分区。
  2. 设 pivot 最终位置为索引 p。
    • 如果 p=k,则该元素即为第 k小。
    • 如果 p>k,在左半区继续查找第 k 小。
    • 如果 p<k,在右半区查找第 k−(p−l+1)小。
  3. ps:把快排稍微改改就能用了
int partition(int arr[], int low, int high) {
    int pivot = arr[low]; // 选择第一个元素作为基准
    int left = low + 1;
    int right = high;

    while (true) {
        // 找到左侧第一个大于基准的元素
        while (left <= right && arr[left] <= pivot) {
            left++;
        }
        // 找到右侧第一个小于基准的元素
        while (left <= right && arr[right] >= pivot) {
            right--;
        }
        // 如果左右指针交错,退出循环
        if (left > right) {
            break;
        }
        // 交换左侧和右侧的元素
        swap(arr[left], arr[right]);
    }
    // 将基准放到正确的位置
    swap(arr[low], arr[right]);
    return right;
}

// 在乱序数组中查找第k小(k从1开始)
int select_kth(int arr[], int low, int high, int k) {
    if (low == high) {
        return arr[low]; // 区间只有一个元素,直接返回
    }
    int pi = partition(arr, low, high); // 基准的绝对索引
    int cnt = pi - low + 1; // 基准在当前区间的相对排名(从1开始)

    if (cnt == k) {
        return arr[pi]; // 正好是第k小
    } else if (cnt > k) {
        return select_kth(arr, low, pi - 1, k); // 在左半部分找
    } else {
        return select_kth(arr, pi + 1, high, k - cnt); // 在右半部分找第(k-cnt)小
    }
}

3.4 求解组合问题

3.4.1 求解最大子段和问题

P1115 最大子段和 - 洛谷

给出一个长度为 n 的序列 a,选出其中连续且非空的一段使得这段和最大。

以下给出两个方法的实现

分治法(O(n log n))

  • 每一次分成左右两个区间,则最大子段和有三种情况,仅左区间,仅右区间,跨越中间
  • 第三种情况维护一下中点往左右分别的最大前后缀和即可
int _calc(int l,int mid,int r,vector<int> &a){
	int tl=0,tr=0;
	int maxnl=-1e12,maxnr=-1e12;
	for(int i=mid-1;i>=l;i--){
		tl+=a[i];
		maxnl=max(maxnl,tl);
	}
	for(int i=mid+1;i<=r;i++){
		tr+=a[i];
		maxnr=max(maxnr,tr);
	}
	return maxnl+maxnr+a[mid];
}
int calc(int l,int r,vector<int> &a){
	int mid=(l+r)>>1;
	if(l==r) return a[l];
	int lmaxn=calc(l,mid,a);
	int rmaxn=calc(mid+1,r,a);
	int midmaxn=_calc(l,mid,r,a);
	return max({lmaxn,midmaxn,rmaxn});
}
void solve(){
	int n;cin>>n;
	vector<int> a(n+1);
	for(int i=1;i<=n;i++) cin>>a[i];
	int ans=calc(1,n,a);
	cout<<ans<<endl;
}

动态规划(O(n))

  • 比较好想,f[i]表示以i为结尾的最大子段和,若f[i-1]+a[i]表示连接到上一段后,a[i]表示新开一段,比较一下大小即可
//这里单独处理i=1,以及设定ans初始值为极小负数原因是题目不允许选择空序列,若允许空序列ans设为0即可
void solve(){
	int n;cin>>n;
	vector<int> a(n+1);
	vector<int> f(n+1,0);
	for(int i=1;i<=n;i++) cin>>a[i];
	for(int i=1;i<=n;i++){
		if(i==1){
			f[1]=a[1];
			continue;
		}
		f[i]=max(f[i-1]+a[i],a[i]);
	}
	int res=-1e12;
	for(int i=1;i<=n;i++) res=max(res,f[i]);
	cout<<res<<endl;
}

3.4.2 求解棋盘覆盖问题

P1228 地毯填补问题 - 洛谷

给定一个$ 2^{k}\times 2^{k} $的棋盘,棋盘上有且仅有一个特殊方格。要求使用如下"L"型骨牌(覆盖三个格子)覆盖除特殊方格以外的所有方格,且骨牌不重叠。

分治思路:

  1. 将棋盘分为四个大小为$ 2^{k-1}\times 2^{k-1} $的子棋盘。
  2. 在棋盘中心的四个格子中,“挖空”与特殊格所在象限对应的小格;其余三个中心格放一块"L"形骨牌,使得它们的三角区域与特殊格所在象限相连。
  3. 递归地对四个子棋盘重复上述过程。
  4. 就大模拟,有一点恶心,注意一下坐标什么时候要+1,每个状态注意判断一下当前分割后的棋盘内的特殊点在哪个象限,依据这个来选择方块即可
// x1,y1特殊点坐标 x2,y2左上角点坐标 n长度
void dfs(int x1, int y1, int x2, int y2, int n) {
    if(n == 1) return;
    int t = n / 2;
    
    if(x1 - x2 < t) {
        if(y1 - y2 < t) { // 特殊点在第一象限
            ans.push_back({x2+t, y2+t, 1});
            dfs(x1, y1, x2, y2, t);
            dfs(x2+t-1, y2+t, x2, y2+t, t);
            dfs(x2+t, y2+t-1, x2+t, y2, t);
            dfs(x2+t, y2+t, x2+t, y2+t, t);
        }
        else { // 特殊点在第三象限
            ans.push_back({x2+t, y2+t-1, 2});
            dfs(x2+t-1, y2+t-1, x2, y2, t);
            dfs(x1, y1, x2, y2+t, t);
            dfs(x2+t, y2+t-1, x2+t, y2, t);
            dfs(x2+t, y2+t, x2+t, y2+t, t);
        }
    }
    else {
        if(y1 - y2 < t) { // 特殊点在第二象限
            ans.push_back({x2+t-1, y2+t, 3});
            dfs(x2+t-1, y2+t-1, x2, y2, t);
            dfs(x2+t-1, y2+t, x2, y2+t, t);
            dfs(x1, y1, x2+t, y2, t);
            dfs(x2+t, y2+t, x2+t, y2+t, t);
        }
        else { // 特殊点在第四象限
            ans.push_back({x2+t-1, y2+t-1, 4});
            dfs(x2+t-1, y2+t-1, x2, y2, t);
            dfs(x2+t-1, y2+t, x2, y2+t, t);
            dfs(x2+t, y2+t-1, x2+t, y2, t);
            dfs(x1, y1, x2+t, y2+t, t);
        }
    }
}

void solve() {
    int k, x, y;
    cin >> k >> x >> y;
    ans.clear();
    dfs(x, y, 1, 1, 1<<k);
    for(auto [x, y, t] : ans) {
        cout << x << " " << y << " " << t << endl;
    }
}

时间复杂度为\(O(4^k)\),k为最后切割几次

image-20250602122619910

3.5 求解集合问题

3.5.1 求解最近点对问题

P1257 平面上的最接近点对 - 洛谷

给定平面上n个点,求欧氏距离最小的点对。

暴力法

枚举所有点对,计算距离,取最小。时间复杂度 \(O(n^{2})\)

分治法(经典算法)

  1. 将所有点按x坐标排序后,递归地将点集分为左右两部分,分别求出最近距离。
  2. \(\delta=\min(d_{\text{left}}, d_{\text{right}})\)
  3. 从中线两侧筛选出距离中线水平距离小于δ的点,按y坐标排序后再"滑窗"查找,每个点只需与后续最多6个点比较(不是很会证明这个啊)。
//我不想自己写了,这个代码里没体现最多六个点比较,而且我也不会证明,这样挺快的了
#include <cmath>
#include <cstdio>
#include <iostream>
#include <algorithm>
using namespace std;

struct Point {
    double x, y;
} a[2000005];

int t[2000005];

bool cmp_x(const Point &A, const Point &B) {
    return A.x < B.x;
}
bool cmp_y(int i, int j) {
    return a[i].y < a[j].y;
}

// 递归函数:在 a[l..r] 中求最近点对距离
double closest_pair(int l, int r) {
    if (l == r) return 1e18;
    if (l + 1 == r) {
        double dx = a[l].x - a[r].x;
        double dy = a[l].y - a[r].y;
        return sqrt(dx * dx + dy * dy);
    }
    int mid = (l + r) / 2;
    double d_left = closest_pair(l, mid);
    double d_right = closest_pair(mid + 1, r);
    double d = min(d_left, d_right);
    int cnt = 0;
    for (int i = l; i <= r; i++) {
        if (fabs(a[mid].x - a[i].x) < d) {
            t[cnt++] = i;
        }
    }
    sort(t, t + cnt, cmp_y);
    for (int i = 0; i < cnt; i++) {
        for (int j = i + 1; j < cnt; j++) {
            if (a[t[j]].y - a[t[i]].y > d) break;
            double dx = a[t[i]].x - a[t[j]].x;
            double dy = a[t[i]].y - a[t[j]].y;
            d = min(d, sqrt(dx * dx + dy * dy));
        }
    }
    return d;
}

int main() {
    int n;
    cin >> n;
    for (int i = 0; i < n; i++) {
        scanf("%lf %lf", &a[i].x, &a[i].y);
    }
    sort(a, a + n, cmp_x);
    double ans = closest_pair(0, n - 1);
    printf("%.4lf\n", ans);
    return 0;
}

时间复杂度:O(nlog⁡n)。
空间复杂度:O(n)(用于排序及辅助数组)。

image-20250602132440241

image-20250602132452006

  • 解法很显然,直接sort一下前n/2一边后n/2一边,注意一下奇偶情况即可,重点是算法的基本设计思想怎么写

  • 参考答案:将最小的n/2个元素放在\(A_{1}\)中,其余的元素放在\(A_{2}\)中。该算法基于快速排序思想,根据枢轴位置\(i\)分三种情况处理:

    • \(i=n/2\),划分成功,算法结束

    • \(i<n/2\),枢轴及之前元素属于\(A_{1}\),继续划分\(i\)后元素

    • \(i>n/2\),枢轴及之后元素属于\(A_{2}\),继续划分\(i\)前元素

第四章 动态规划

动态规划法使用条件

  1. 最优子结构(Optimal Substructure)
    问题的最优解包含其子问题的最优解。

  2. 重叠子问题(Overlapping Subproblems)
    子问题之间相互重叠,重用先前计算结果能提高效率。

  3. 无后效性(No After-Effect)
    某阶段状态一旦确定,就不受后续决策影响,只与当前状态有关。

动态规划与分治法的异同

相同点

  • 都是将待求解问题分解成若干子问题
  • 先求解子问题的解,然后通过这些子问题的解得到整个问题的解

不同点

分治法

  • 子问题相互独立
  • 若子问题不独立会导致重复求解,效率较低

动态规划

  • 子问题之间一般具有关联性
  • 为避免重复计算,对每个子问题仅求解一次
  • 将结果存储在"表格"中,后续使用时直接存取

动态规划算法使用步骤

image-20250602144911699

4.1 多段图问题

image-20250602144604251

  • 设f[i]表示到达i点时的最小值,则f[i]=min(f[j]+v(j,i)),从a点开始搜,bfs序即可,主要一下记录路径,到达终点后一步一步回溯即可,具体看代码吧
  • ps:我实在找不到原题,造了一下图片里的数据,亲测是对的
struct node{
	int v,c;
};
void solve(){
	int n,m;cin>>n>>m;
	vector<vector<node>> g(n+1);
	for(int i=1;i<=m;i++){
		int u,v,c;cin>>u>>v>>c;
		g[u].push_back({v,c});
	}
	vector<int> f(n+1,1e9);
	vector<int> path(n+1,0);
	path[1]=-1;
	f[1]=0;
	queue<int> q;
	q.push(1);
	while(!q.empty()){
		auto u=q.front();
		q.pop();
		for(auto [v,c]:g[u]){
			if(f[v]==1e9) q.push(v);
			if(f[u]+c<f[v]){
				f[v]=f[u]+c;
				path[v]=u;
			}
		}
	}
	cout<<f[n]<<endl;
	vector<int> res;
	int id=n;
	while(id!=-1){
		res.push_back(id);
		id=path[id];
	}
	reverse(res.begin(),res.end());
	for(auto x:res) cout<<x<<" ";
	cout<<endl;
}
/*
Input 
10 19
1 2 2
1 3 4
1 4 3
2 5 7
2 6 4
3 5 3
3 6 2
3 7 4
4 5 6
4 6 2
4 7 5
5 8 3
5 9 4
6 8 6
6 9 3
7 8 3
7 9 3
8 10 3
9 10 4

Output
12
1 4 6 9 10 
*/

时间复杂度\(O(nm)\)

  • 我找了一个差不多的题目,但是会稍微难一点,也可以看一下

UVA116 单向TSP Unidirectional TSP - 洛谷

UVA116 单向TSP Unidirectional TSP

题目描述

给一个m行n列(m≤10,n≤100)的整数矩阵,从第一列任何一个位置出发每次往右、右上、右下走一格,最终到达最后一列。要求经过的整数之和最小。整个矩阵是环形的,即第一行的上一行是最后一行,最后一行的下一行是第一行。输出路径上每列的行号。多解时输出字典序最小的。

输入有若干组数据:

每组的第1行:m和n,分别为行数和列数。

每组的第2~m+1行:每行n个数,用空格分开,代表整数矩阵。

输出:

每组有两行,第一行是每列的行号,第二行是路径的经过的整数之和。

输入 #1

5 6
3 4 1 2 8 6
6 1 8 2 7 4
5 9 3 9 9 5
8 4 1 3 2 6
3 7 2 8 6 4
5 6
3 4 1 2 8 6
6 1 8 2 7 4
5 9 3 9 9 5
8 4 1 3 2 6
3 7 2 1 2 3
2 2
9 10 9 10

输出 #1

1 2 3 4 4 5
16
1 2 1 5 4 5
11
1 1
19
  • 也比较简单的一个题目状态定义 dp(x, y) 表示以矩阵第 x 行第 y 列为起点,通向最后一行的最小路径值,则状态转移方程为:

  • \[dp(x, y) = \min \begin{cases} dp((x + 1) \% m, \ y + 1) \\ dp(x, \ y + 1) \\ dp((x - 1 + m) \% m, \ y + 1) \end{cases} + mp[x][y] \]

  • 难点在于如何记录路径,看看代码吧

#include <bits/stdc++.h>
#define endl '\n'
using namespace std;
const int N = 110;
const int M = 15;
int g[M][N], f[M][N];
bool G[M][N][M][N];
int m, n;

void Print(int x, int y) {
    cout << (y == 0 ? "" : " ") << x + 1;
    for (int i = 0; i < m; i++) {
        for (int j = 0; j < n; j++) {
            if (G[x][y][i][j] && f[i][j] == f[x][y] - g[x][y]) {
                Print(i, j);
                return;
            }
        }
    }
}

void solve() {
    while (cin >> m >> n) {
        // 清空数组
        memset(g, 0, sizeof(g));
        memset(f, 0, sizeof(f));
        memset(G, 0, sizeof(G));

        // 输入矩阵
        for (int i = 0; i < m; i++) {
            for (int j = 0; j < n; j++) {
                cin >> g[i][j];
            }
        }

        // 初始化最后一行
        for (int i = 0; i < m; i++) {
            f[i][n - 1] = g[i][n - 1];
        }

        // 动态规划计算最小路径
        for (int j = n - 2; j >= 0; j--) {
            for (int i = 0; i < m; i++) {
                f[i][j] = min({f[(i + 1) % m][j + 1], f[i][j + 1], f[(i - 1 + m) % m][j + 1]}) + g[i][j];
            }
        }

        // 标记可达路径
        for (int i = 0; i < m; i++) {
            for (int j = 0; j < n; j++) {
                G[i][j][(i + 1) % m][j + 1] = true;
                G[i][j][i][j + 1] = true;
                G[i][j][(i - 1 + m) % m][j + 1] = true;
            }
        }

        // 寻找最小路径起点
        int ans = INT_MAX;
        int idx = 0;
        for (int i = 0; i < m; i++) {
            if (ans > f[i][0]) {
                idx = i;
                ans = f[i][0];
            }
        }

        // 输出结果
        Print(idx, 0);
        cout << endl << ans << endl;
    }
}

int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);
    solve();
    return 0;
}

4.2 求解数塔问题(三角形最长路径问题)

给定一个如下的数塔,从最顶层走到最底层,每一步只能向左下或右下移动,求路径上的最大和。

信息学奥赛一本通(C++版)在线评测系统

image-20250602154834445

  • 很显然了,f[i][j]表示到(i,j)时的最大值,直接转移就好
//自顶向下 PPT里叫做备忘录方法  
void solve(){
	int n;cin>>n;
	vector<vector<int>> g(n+1,vector<int>(n+1,0));
	for(int i=1;i<=n;i++){
		for(int j=1;j<=i;j++){
			cin>>g[i][j];
		}
	}
	vector<vector<int>> f(n+1,vector<int>(n+1,0));
	for(int i=1;i<=n;i++){
		for(int j=1;j<=i;j++){
			f[i][j]=max(f[i-1][j-1],f[i-1][j])+g[i][j];
		}
	}
	int res=0;
	for(int j=1;j<=n;j++){
		res=max(res,f[n][j]);
	}
	cout<<res<<endl;

}
//自底向上 动态规划
void solve(){
	int n;cin>>n;
	vector<vector<int>> g(n+1,vector<int>(n+5,0));
	for(int i=1;i<=n;i++){
		for(int j=1;j<=i;j++){
			cin>>g[i][j];
		}
	}
	vector<vector<int>> f(n+1,vector<int>(n+5,0));
	for(int j=1;j<=n;j++) f[n][j]=g[n][j];
	for(int i=n-1;i>=1;i--){
		for(int j=1;j<=i;j++){
			f[i][j]=max(f[i+1][j],f[i+1][j+1])+g[i][j];
		}
	}
	int res=f[1][1];
	cout<<res<<endl;
}

时间复杂度为O(\(n^2\)

备忘录方法:采用自顶向下的递归算法,设置了一个备忘录,初始的时候,设置一个特殊值,表示没被
求过,每次都查看备忘录,如果不是特殊值,就直接取了,也被称为搜表法。

4.3 LIS 最长上升子序列

B3637 最长上升子序列 - 洛谷

  • ppt里只要求O(\(n^2\))就可以了,那就很简单了f[i]表示以i结尾的最长上升子序列的长度,随便转移
void solve(){
	int n;cin>>n;
	for(int i=1;i<=n;i++) cin>>a[i];
	for(int i=1;i<=n;i++) f[i]=1;
	f[0]=0;
	for(int i=1;i<=n;i++){
		for(int j=1;j<i;j++){
			if(a[j]<a[i]) f[i]=max(f[i],f[j]+1);
		}
	}
	int ans=0;
	for(int i=1;i<=n;i++)
		ans=max(ans,f[i]);
	cout<<ans<<endl;
}

时间复杂度为O(\(n^2\)

4.4 LCS 最长公共子序列

  • 就用小雅上的练习题吧,这个还要求回溯的,比较符合我们的要求

image-20250602234505792

  • 怎么做呢?这题要深入理解LCS的原理,在求LCS的过程中,我们要标记每个字符间的关系,然后递归的输出,但是小雅的数据很水,我觉得考试也不会考这么难,最重要的是我杭电账号登陆不上去,测试不了难的数据,那就写一个简单的吧,就求出LCS之后把其他非LCS的部分按顺序加入ans就可以了,时间复杂度还是比较大,因为字符串的函数还是比较慢的,过小雅肯定是没问题了,有兴趣的可以去看一下加强版:http://acm.hdu.edu.cn/showproblem.php?pid=1503

  • 可以简单讲一下LCS,我们可以用dp[i][j]来表示第一个串的前i位,第二个串的前j位的LCS的长度

  • 那么我们是很容易想到状态转移方程的: 如果当前的A1[i]和A2[j]相同(即是有新的公共元素) 那么 dp[i][j]=max(dp[i][j],dp[i−1][j−1]+1);

  • 如果不相同,即无法更新公共元素,考虑继承: dp[i][j]=max(dp[i−1][j],dp[i][j−1]

  • 至于如何回溯呢,实际上f数组会被刷成一个二维的表,从(n,m)转移回去就可以了,不多说下见代码

void solve(){
	string s;cin>>s;
	string t;cin>>t;
	int n=s.size();
	int m=t.size();
	s=" "+s;
	t=" "+t;
	vector<vector<int>> f(n+1,vector<int>(m+1,0));
	for(int i=1;i<=n;i++){
		for(int j=1;j<=m;j++){
			f[i][j]=max(f[i-1][j],f[i][j-1]);
			if(s[i]==t[j]){
				f[i][j]=max(f[i][j],f[i-1][j-1]+1);
			}
		}
	}
	int i=n,j=m;
	string res="";
	while(i>=1 && j>=1){
		if(s[i]==t[j]){
			res+=s[i];
			i--,j--;
		}
		else if(f[i-1][j]>f[i][j-1]){//由max转移过来的
			i--;
		}
		else j--;
	}
	reverse(res.begin(),res.end());
	int k=res.size();
	string ans="";
	for(int j=0;j<k;j++){
		int id1=s.find(res[j]);
		int id2=t.find(res[j]);
		if(j==0){
			if(id1!=-1){
				ans+=s.substr(1,id1-1);
				s.erase(0,id1+1);
			}
			if(id2!=-1){
				ans+=t.substr(1,id2-1);
				t.erase(0,id2+1);
			}
			ans+=res[j];
		}
		else{
			if(id1!=-1){
				ans+=s.substr(0,id1);
				s.erase(0,id1+1);
			}
			if(id2!=-1){
				ans+=t.substr(0,id2);
				t.erase(0,id2+1);
			}
			ans+=res[j];
		}
	}
	ans+=s;
	ans+=t;
	cout<<ans<<endl;
}

4.5 01 背包问题

  • 不谈了,直接看代码吧,注意滚动的倒序枚举,没滚动的可以增加一个回溯求解的部分
//没滚动 + 求出最优解
void solve(){
	int n,m;cin>>n>>m;
	vector<int> w(n+1);
	vector<int> v(n+1);
	vector<vector<int>> f(n+1,vector<int>(m+1,0));
	for(int i=1;i<=n;i++) cin>>w[i];
	for(int i=1;i<=n;i++) cin>>v[i];
	for(int i=1;i<=n;i++){
		for(int j=1;j<=m;j++){
			f[i][j]=f[i-1][j];
			if(j-w[i]>=0){
				f[i][j]=max(f[i][j],f[i-1][j-w[i]]+v[i]);
			}	
		}
	}
	// for(int i=1;i<=n;i++){
		// for(int j=1;j<=m;j++){
			// cout<<f[i][j]<<" ";
		// }
		// cout<<endl;
	// }
	int i=n,j=m;
	vector<int> vis(n+1,0);
	while(i>=1){
		if(f[i][j]!=f[i-1][j]){
			vis[i]=1;
			j-=w[i];
		}
		i--;
	}
	for(int i=1;i<=n;i++) cout<<vis[i]<<" ";
	cout<<endl;

}
//滚动优化空间复杂度
void solve(){
	int n,m;cin>>n>>m;
	vector<int> w(n+1);
	vector<int> v(n+1);
	vector<int> f(m+1,0);
	for(int i=1;i<=n;i++) cin>>w[i];
	for(int i=1;i<=n;i++) cin>>v[i];
	for(int i=1;i<=n;i++){
		for(int j=m;j>=w[i];j--){
			f[j]=max(f[j],f[j-w[i]]+v[i]);
		}
	}
	cout<<f[m]<<endl;

}

时间复杂度为O(\(n^2\))

4.6 旅行商问题 (TSP)

  • 首先明确一下TSP问题是NP-HARD问题,没有多项式的算法时间复杂度

问题描述

有若干个城市,任何两个城市之间的距离都是确定的,现要求一旅行商从某城市出发必须经过每一个城市且只在一个城市逗留一次,最后回到出发的城市,问如何事先确定一条最短的线路已保证其旅行的费用最少?

暴力法

枚举出所有的旅行路线进行比较,时间复杂度O(n!)

动态规划法

  • 我觉得不会考这个,这个还要用到状压有点难的

  • 简单讲一下原理:

    假设从顶点s出发,令d(i,V)表示从顶点i出发经过集合V中各顶点一次且仅一次,最后回到出发点s的最短路径长度。

    推导过程(分情况讨论)

    1. V为空集时
      表示直接从i回到s,此时:

      \[d(i,V) = c_{is} \quad (i \neq s) \]

    2. V不为空集时
      需对子问题求解,遍历集合V中的每个城市k并求最优解:

      \[d(i,V) = \min \left( c_{ik} + d(k,V \setminus \{k\}) \right) \]

      注:

      • c_{ik}:城市i到城市k的距离
      • d(k,V\{k\}):子问题的最短路径

    动态规划方程

    综上,TSP问题的动态规划方程为:

    \[d(i,V) = \begin{cases} c_{is}, & V = \varnothing, i \neq s \\ \min \left\{ c_{ik} + d(k,V \setminus \{k\}) \right\}, & k \in V, V \neq \varnothing \end{cases} \]

    其中s为起点。

    可以通过下面这个图加深理解

    image-20250603102846471

#include<bits/stdc++.h>
#define endl '\n'
using namespace std;
const int N = 5;
const int INF = 1e9;
const int M = 1 << (N-1);

int g[N][N] = {
    {0,3,INF,8,9},
    {3,0,3,10,5},
    {INF,3,0,4,3},
    {8,10,4,0,20},
    {9,5,3,20,0}
};

int dp[N][M];
vector<int> path;

void tsp() {
    for(int i = 0; i < N; ++i) dp[i][0] = g[i][0];
    //状态压缩,用n位二进制数表示所有点集,1表示在点集中
    for(int j = 1; j < M; ++j) {
        for(int i = 0; i < N; ++i) {
            dp[i][j] = INF;
            if((j >> (i-1)) & 1) continue;  //j包含i,跳过
            for(int k = 1; k < N; ++k) {  //从i走到k
                if(!((j >> (k-1)) & 1)) continue; //j不包含k,跳过,即此时k要枚举点集里的点
                dp[i][j] = min(dp[i][j], g[i][k] + dp[k][j ^ (1 << (k-1))]);//异或把点k从集合里拿出来
            }
        }
    }
}

bool check(bool vis[]) {
    for(int i = 1; i < N; ++i)
        if(!vis[i]) return false;
    return true;
}

void get_path() {
	//依据dp的表找出路径,具体来说从起始点开始,每次确定下一个点的位置,确定的方式就是看在转移过来的
    bool vis[N] = {0};
    int u = 0, S = M - 1;
    path.push_back(0);
    while(!check(vis)) {
        int nxt = -1, minv = INF;
        for(int v = 1; v < N; ++v) {
            if(!vis[v] && (S & (1 << (v-1)))) {
                if(g[v][u] + dp[v][S ^ (1 << (v-1))] < minv) {
                    minv = g[v][u] + dp[v][S ^ (1 << (v-1))];
                    nxt = v;
                }
            }
        }
        u = nxt;
        path.push_back(u);
        vis[u] = true;
        S ^= (1 << (u-1));
    }
}

void print() {
    cout << dp[0][M-1] << endl;
    for(auto x : path) cout << x << " ";
    cout << "0\n";
}
void solve(){
	tsp();
    get_path();
    print();
}
int main(){
	std::ios::sync_with_stdio(false);
	cin.tie(0);cout.tie(0);
	int T=1;
	while(T--) solve();
	return 0;
}

时间复杂度为O(\(2^n\)\(n^2\))

第五章 贪心法

贪心算法:在每个阶段都做出当前最优的局部选择,最终希望得到全局最优解。并非所有问题都能用贪心法求解,但若问题满足以下性质,则可使用贪心法。

  • 最优子结构:全局最优解包含局部最优选择。
  • 贪心选择性质:从当前状态做出局部最优选择后,剩余子问题依然能通过类似策略得到全局最优解。

贪心算法与动态规划 (DP) 的异同

不同点

1. 求解思路

特性 动态规划 (DP) 贪心算法
求解方向 自底向上 自顶向下
依赖关系 依赖子问题的解 不依赖子问题的解
选择策略 在子问题解的基础上做出选择 仅基于当前状态做局部最优选择

2. 适用问题

算法 必要条件 特点
动态规划 最优子结构 + 重叠子问题 需要存储子问题解
贪心算法 最优子结构 + 贪心选择性质 局部最优能导致全局最优

相同点

  1. 都用于求解最优化问题
  2. 都需要问题具有最优子结构性质
  3. 都通过做出一系列决策来解决问题
  4. 都是算法设计中重要的策略模式

5.2 图着色问题(GCP)

  • 如果只分两种颜色我会做,二分图匹配,如果大于等于三种,那这也是一个NP-HARD问题

    image-20250603105442934

  • 贪心法就是,枚举所有颜色,扫一遍所有点,当前点能涂颜色涂颜色,当然这不一定对

void solve(){
	int n,m,k;cin>>n>>m>>k;
	vector<vector<int>> g(n+1,vector<int>());
	vector<int> res(n+1,0);
	for(int i=1;i<=m;i++){
		int u,v;cin>>u>>v;
		g[u].push_back(v);
		g[v].push_back(u);
	}
	for(int i=1;i<=k;i++){
		for(int j=1;j<=n;j++){
			if(res[j]) continue;
			bool flag=1;
			for(auto v:g[j]){
				if(res[v]==k){
					flag=0;
					break;
				}
			}
			if(flag){
				res[j]=k;
				break;
			}
		}
	}
}

时间复杂度O(nk)

5.3 TSP问题

  • 我又来了,显然贪心法更简单,时间复杂度也更低,就是准确率我觉得和随机算法差不多啊,不写代码了,看看伪代码吧
  1. 最近邻点策略:从某城市出发,每次在没有到过的城市中选择最近的一个,直到经过了所有的城市,最后回到出发城市。

image-20250603110635756

  1. 最短链接策略:每次在整个图的范围内选择最短边加入到解集合中,但是,要保证加入解集合中的边最终形成一个哈密顿回路。因此,当从剩余边集E'中选择一条边(u, v)加入解集合S中,应满足以下条件:

    ① 边(u, v)是边集E'中代价最小的边;

    ② 边(u, v)加入解集合S后,S中不产生回路;

    ③ 边(u, v) 加入解集合S后,S中不产生分枝(就是没有节点度数超过3);

    image-20250603112020154

    image-20250603112045652

时间复杂度O(\(n^2\)),第二种方法用并查集优化可以到O(nlogn)

5.4 求解部分背包问题

image-20250603112453861

  • 按单位价值排序即可
void solve(){
	int n,C;cin>>n>>C;
	vector<int> w(n+1),v(n+1);
	vector<double> p(n+1);
	for(int i=1;i<=n;i++) cin>>w[i];
	for(int i=1;i<=n;i++) cin>>v[i];
	for(int i=1;i<=n;i++) p[i]=1.0*v[i]/w[i];
	sort(p.begin()+1,p.end(),greater<double>());
	int ans=0;
	for(int i=1;i<=n;i++){
		if(C>=w[i]){
			C-=w[i];
			ans+=v[i];
		}
		else{
			ans+=C*p[i];
			C=0;
		}
	}
	cout<<ans<<endl;
}

时间复杂度O(nlogn)

5.5 求解活动安排问题(独立区间问题)

image-20250603113028194

P1803 凌乱的yyy / 线段覆盖 - 洛谷

  • 比较好想的贪心,按结束时间排序即可
struct line{
	int x,y;
}a[N];
bool cmp(line p,line q){
	if(p.y==q.y) return p.x<q.x;
	else return p.y<q.y;
}
void solve(){
	int n;cin>>n;
	for(int i=1;i<=n;i++){
		cin>>a[i].x>>a[i].y;
	}
	sort(a+1,a+n+1,cmp);
	int prel=0;
	int ans=0;
	for(int i=1;i<=n;i++){
		if(a[i].x>=prel){
			prel=a[i].y;
			ans++;
		}
	}
	cout<<ans<<endl;
}

时间复杂度O(nlogn)

5.6 多机调度问题

  • 这也是NP-HARD

  • 贪心策略最长处理时间作业优先,即把处理时间最长的作业分配给最先空闲的机器,这样可以保证处理时间长的作业优先处理,从而在整体上获得尽可能短的处理时间。看个伪代码吧,具体实现可以用一个小顶堆维护一下

    image-20250603114351698

    时间复杂度O(nlogn)

5.7 Huffman 编码

给定若干字符及其权值(出现频率),构造一棵最优二叉编码树,使得加权路径长度(WPL)最小。

  • 这个也学过好多次了,不多赘述,看代码
#include <iostream>
#include <queue>
#include <vector>
#include <string>
#include <unordered_map>
using namespace std;

struct Node {
    char ch;
    int freq;
    Node *left, *right;
    Node(char c, int f) : ch(c), freq(f), left(nullptr), right(nullptr) {}
};

struct Compare {
    bool operator()(Node* a, Node* b) {
        return a->freq > b->freq;
        //如果为真的话,则b在a前面,这是自定义比较器的语法
        //如果为假,则保持a,b顺序
    }
};

void buildHuffmanTree(const string& s) {
    // 统计频率
    unordered_map<char, int> freq;
    for (char c : s) freq[c]++;

    // 优先队列(最小堆)
    priority_queue<Node*, vector<Node*>, Compare> pq;
    for (auto& p : freq) 
        pq.push(new Node(p.first, p.second));

    // 构建Huffman树
    while (pq.size() > 1) {
        Node* left = pq.top(); pq.pop();
        Node* right = pq.top(); pq.pop();
        Node* parent = new Node('\0', left->freq + right->freq);
        parent->left = left;
        parent->right = right;
        pq.push(parent);
    }

    // 生成编码(DFS遍历)
    unordered_map<char, string> codes;
    function<void(Node*, string)> dfs = [&](Node* root, string path) {
        if (!root) return;
        if (root->ch != '\0') codes[root->ch] = path;
        dfs(root->left, path + "0");
        dfs(root->right, path + "1");
    };
    dfs(pq.top(), "");

    // 输出结果
    cout << "字符 频率 编码\n";
    for (auto& p : codes) 
        cout << p.first << "    " << freq[p.first] << "    " << p.second << "\n";
}

int main() {
    ios::sync_with_stdio(false);
    string s = "abracadabra";
    buildHuffmanTree(s);
    return 0;
}

时间复杂度O(nlogn)

5.8 Dijkstra 算法

P4779 【模板】单源最短路径(标准版) - 洛谷

  • 最短路,不多赘述
struct node{
	int to,w;
	bool operator< (const node rhs)const{
		return w>rhs.w;
	}
};
void solve(){
	int n,m,s;cin>>n>>m>>s;
	vector<vector<node>> g(n+1,vector<node>());
	vector<int> dist(n+1,1e9);
	vector<int> vis(n+1,0);
	for(int i=1;i<=m;i++){
		int u,v,w;cin>>u>>v>>w;
		g[u].push_back({v,w});
	}
	priority_queue<node> q;
	dist[s]=0;
	q.push({s,0});
	while(!q.empty()){
		auto t=q.top();
		q.pop();
		int u=t.to;
		if(vis[u]) continue;
		vis[u]=1;
		for(auto tmp:g[u]){
			auto [v,w]=tmp;
			if(dist[v]>dist[u]+w){
				dist[v]=dist[u]+w;;
				q.push({v,dist[v]});
			}
		}
	}
	for(int i=1;i<=n;i++){
		cout<<dist[i]<<" ";
	}
}

时间复杂度O(nlogn)

5.9 最小生成树

P3366 【模板】最小生成树 - 洛谷

Kruskal算法

  1. 将所有边按权值从小到大排序。

  2. 使用并查集(Union-Find)维护连通性。

  3. 依次取最小边,若加入该边不会形成环,则将其加入生成树。

  4. 重复直到生成树有 n−1 条边。

#include <bits/stdc++.h>
using namespace std;
#define int long long
const int N = 2e5 + 10;

struct Edge {
    int u, v, w;
} e[N];
int fa[N], n, m, ans, cnt;

bool cmp(Edge a, Edge b) {
    return a.w < b.w;
}

int find(int x) {
    if (x != fa[x]) return fa[x] = find(fa[x]);
    return fa[x];
}

void kruskal() {
    sort(e + 1, e + m + 1, cmp);
    for (int i = 1; i <= m; i++) {
        int px = find(e[i].u), py = find(e[i].v);
        if (px == py) continue;
        ans += e[i].w;
        fa[px] = py;
        cnt++;
        if (cnt == n - 1) break;
    }
}

signed main() {
    cin >> n >> m;
    for (int i = 1; i <= n; i++) fa[i] = i;
    for (int i = 1; i <= m; i++) {
        cin >> e[i].u >> e[i].v >> e[i].w;
    }
    kruskal();
    cout << (cnt == n - 1 ? ans : -1) << endl;
    return 0;
}

时间复杂度O(mlogm)

Prim 算法

  1. 任意选定一个起始顶点,加入生成树集合。

  2. 重复:在“生成树集合”与“其余顶点”之间选取权值最小的横切边,加入生成树。

  3. 直到所有顶点都被包含。

  • 注意,这里dist[i]表示的是到达i点选择的那一条边的长度,而不是路线总长度
struct node{
	int to,w;
	bool operator < (const node rhs) const{
		return w>rhs.w;
	}
};
void solve(){
	int n,m;cin>>n>>m;
	vector<vector<node>> g(n+1,vector<node>());
	for(int i=1;i<=m;i++){
		int u,v,w;cin>>u>>v>>w;
		g[u].push_back({v,w});
		g[v].push_back({u,w});
	}
	priority_queue<node> q;
	vector<int> dist(n+1,1e9);
	vector<int> vis(n+1,0);
	dist[1]=0;
	q.push({1,0});
	int cnt=0;
	int ans=0;
	while(!q.empty() && cnt<=n){
		auto t=q.top(); 
		q.pop();
		auto [u,d]=t;
        if(vis[u]) continue;
        cnt++;
        ans+=d;
        vis[u]=1;
        for(auto tmp:g[u]){
        	auto [v,w]=tmp;
        	if(!vis[v] && w<dist[v]){
        		dist[v]=w;
        		q.push({v,dist[v]});
        	}
        }
	}
	if (cnt == n) cout << ans << endl;
    else cout << "orz" << endl;

}

时间复杂度O(nlogn)

接下来两章不考代码题,重点在于对中间过程的理解

第六章 回溯法

6.1 回溯法概述

回溯法(Backtracking)是一种"试探"加"回退"的算法思想,其核心要点如下:

  1. 解空间建模
    将问题的解空间抽象为树(或图)结构,每个节点代表一个可能的解状态。

  2. 试探与回退机制

    • 从根节点出发深度优先搜索(DFS)
    • 当某分支不满足约束条件时,立即回溯到上一节点
    • 尝试其他未探索的分支
  3. 终止条件
    搜索直到找到所有可行解或满足特定条件的解为止。

6.1.1 回溯法的设计思想

  • 构造一个“解空间树”,树的每个节点代表一个“部分解”。

  • 从根节点开始深度优先搜索(DFS),当节点不满足条件时立即剪枝(回退)。

  • 若搜索到叶子节点(或满足某种终止条件),则得到一个完整解。

时间复杂度:与分支因子和最大深度密切相关,如全排列问题为 O(n!),子集枚举问题为 O(\(n^2\))。

  • 解空间:问题所有可能解的集合,由解向量的所有可能取值构成
  • 可能解:解空间中任意一个解向量(不考虑约束条件)
  • 可行解:满足问题约束条件的解向量
  • 最优解:在可行解中使目标函数最优的解

image-20250603160259623

  • 解空间树
  • 定义:将问题求解的判断决策过程及可能结果用树结构呈现
  • 根结点:初始状态
  • 内部结点:判断决策过程
  • 叶子结点:最终可能解
  • 路径:从根到叶子的路径对应一个解向量

image-20250603160608022

image-20250603160801374

image-20250603161319368

6.2 求解图的 m 着色问题

给定一个图,顶点数为 n,提供 m 种颜色,判断是否存在一种合法的着色方式,使得相邻顶点颜色不同。

  • 又是我,老熟人,这次我们用dfs来解决
#include<bits/stdc++.h>
#define endl '\n'
using namespace std;
int g[110][110];
int a[110];
int n,m,k,ans=0;
bool check(int x){
	for(int i=1;i<=n;i++){
		if(g[x][i] && a[x]==a[i]) return false;
	}
	return true;
}
void dfs(int x){
	if(x>n){
		ans++;
		return;
	}
	for(int i=1;i<=m;i++){
		a[x]=i;
		if(check(x)) dfs(x+1);
		a[x]=0;
	}
}
void solve(){
	cin>>n>>k>>m;
	for(int i=0;i<k;i++){
		int u,v;cin>>u>>v;
		g[u][v]=g[v][u]=1;
	}
	dfs(1);

}
int main(){
	std::ios::sync_with_stdio(false);
	cin.tie(0);cout.tie(0);
	int T;cin>>T;
	while(T--) solve();
	return 0;
}

时间复杂度最坏会到O(\(m^n\)

image-20250603175117337

6.3 求解 n 皇后问题

在 n×n 棋盘上放置 n个皇后,使其互不攻击(同一行/列/对角线上只能有一个)。求所有解的个数,或输出部分解。

  • 理解一下过程吧,题目可以做一下八皇后的题目

image-20250603180327194

image-20250603181432462

[P1219 USACO1.5] 八皇后 Checker Challenge - 洛谷

#include<bits/stdc++.h>
using namespace std;
int n,cnt=0,vis1[30],vis3[30],vis4[30],ans[30];//vis1列,vis3左下到右上,vis4左上到右下,ans方案 
void dfs(int x){
	if(x>n){
		cnt++;
		if(cnt<=3){
			for(int i=1;i<=n;i++) printf("%d ",ans[i]);printf("\n");
		}
	} 
	for(int i=1;i<=n;i++){
		if(!vis1[i] && !vis3[x+i] && !vis4[x-i+n]){
			ans[x]=i,vis1[i]=1,vis3[x+i]=1,vis4[x-i+n]=1;
			dfs(x+1);
			ans[x]=0,vis1[i]=0,vis3[x+i]=0,vis4[x-i+n]=0;
		}	
	}	
}
int main(){
	scanf("%d",&n);
	dfs(1);
	printf("%d",cnt);
	return 0;
}

时间复杂度为O(n!)

6.4 求解 0-1 背包问题(回溯法)

对第 4 章动态规划中所述的 0-1 背包问题,若 n 较小,也可使用回溯枚举所有可能:

  • 不写代码了就,都搜索了为什么不加个记忆化

image-20250603182646809

image-20250603182934936

image-20250603182804024

  • 这里如何剪枝去看一下ppt怎么写的,也很简单,左剪枝就是看一下容量是不是超了,右剪枝就是看一下剩余的价值能不能比当前最大值还要大

第七章 分支限界法

  • 这一章有很多概念,我觉得仅作了解即可,其实核心思想就是bfs,只不过给加入队列的点添加上一个约束条件,满足条件的再加入队列

7.1 分支限界法概述

7.1.1 什么是分支限界法

  • 定义
    分支限界法(Branch and Bound)是一种基于“广度优先”或“最小耗费优先”策略的搜索方法,借助限界函数(Bound Function)在“解空间树”中进行剪枝,从而加速寻找最优解。
  • 核心思想
    1. 广度优先搜索(BFS)最小限界值优先 的方式,逐层扩展节点。
    2. 每个节点在扩展时,都需要计算一个限界函数(上界或下界)来估计该节点对应子树可能得到的最优解。
    3. 如果限界值表明从该节点出发不可能获得比当前最优解更好的值,则剪去(丢弃)该节点及其子树。
    4. 否则,将该节点加入“活节点表”(Alive List),等待后续进一步扩展。

求解步骤

  1. 确定解的形式,并定义解空间(结构、层次)。
  2. 设计限界函数,得到目标函数可能的取值范围 [down, up]
    • 如果目标是求最大值,则设计“上界函数”(Upper Bound),保证父节点的上界 ≥ 子节点上界。
    • 如果目标是求最小值,则设计“下界函数”(Lower Bound),保证父节点的下界 ≤ 子节点下界。
  3. 按照广度优先策略,在解空间树中扩展节点:
    1. 从“活节点表”中选择一个使目标函数取极值(最大或最小)的节点作为当前扩展节点。
    2. 依次生成其所有子节点,并对每个子节点计算限界值:
      • 若子节点限界值不再目标范围内,则剪枝
      • 否则,将其加入“活节点表”。
  4. 重复步骤 3,直到找到所需最优解或“活节点表”为空。

7.1.2 分支限界法的设计思想

1. 限界函数的设计

  • 目的:快速剪去不可能产生最优解的分支,缩小搜索空间,提高搜索效率。
  • 要求
    1. 计算简单(开销低);
    2. 保证全局最优解仍在搜索空间内(剪枝安全);
    3. 能在搜索早期就对不可能优于当前最优解的节点进行丢弃。
  • 设计方式
    • 求最大值问题:构造一个上界函数 ub(s),若节点 s 的上界 ub(s) < 当前最优值,则剪枝。
    • 求最小值问题:构造一个下界函数 lb(s),若节点 s 的下界 lb(s) > 当前最优值,则剪枝。

2. 活节点表的存储结构

  • 队列式(FIFO)
    • 简单地用普通队列 queue<Node> 存储活节点,严格按照生成顺序扩展。
  • 优先队列式(Priority Queue)
    • 用一个最大/最小堆(根据问题是求最大值还是最小值)存储活节点,出队时优先选取具有最优限界值的节点。

3. 确定最优解的各个分量

  • 分支限界法在解空间树上跳跃式地扩展,若直接回溯到根节点难以重建完整解向量,常用两种做法:
    1. 在每个节点处保存一条完整的路径(解向量),即每个节点的 x[] 数组记录了从根节点到该节点所有分量的取值。
      • 优点:实现简便,获取最优解时直接读取 x[] 即可。
      • 缺点:空间开销大。
    2. 保存父指针,构建子树结构,找到叶节点后通过父指针向上回溯,依次确定每一层的分量取值。
      • 优点:节省每个节点存储完整解向量的空间。
      • 缺点:需要额外维护指针链,编程复杂度略高。

4. 分支限界法与回溯法的主要区别

回溯法 分支限界法
搜索策略 深度优先(DFS) 广度优先或最小限界值优先(BFS/PQ)
节点存储 用栈(隐式调用栈)或显式栈 用队列(FIFO)或优先队列(Priority Queue)
节点扩展 一旦剪枝,直接返回上层 对所有子节点估算限界,再剪枝或入队
目标 找出所有满足条件的解 找到一个可行解或特定意义的最优解
剪枝依据 仅限约束条件 限界函数 + 约束条件

7.2 求解01背包问题

  • 老朋友啊老朋友,这次轮到分支限界法了
  • 代码就不说了,也不会考,讲一下如何设置限界函数,这里用了一个比较简单的方法,回想一下贪心法里的部分背包问题,我们可以用单位价值计算可能的最大上界,那么该01背包的问题的最优值也不会超过他

image-20250603205550424

··························································································image-20250603205741710

image-20250603205955479

image-20250603212108574

image-20250603212203522

7.3 求解TSP问题

  • 老朋友了,直接看ppt吧

image-20250603213139226

image-20250603213214736

  • 具体流程看ppt吧,里面比较详细

DLC 蛮力法 和 减治法

  • 听说不考啊,为了完备性,就不在这里写了,可以自行了解一下KMP算法,蛮力法那个ppt里提到了

  • 减治法就是去掉合并过程的分治法,可以了解一下堆排序

posted @ 2025-06-03 23:58  Persona_owl  阅读(116)  评论(0)    收藏  举报