【九十六】【算法分析与设计】使用回溯法求解子集(幂集)问题,使用回溯法求解“和”问题,使用回溯法求解全排列问题,使用回溯法求解0/1背包问题(同时进行左剪枝和右剪枝改进)

使用回溯法求解子集(幂集)问题

有一个含n个整数的数组a,所有元素均不相同,设计一个算法求其所有子集(幂集)。

例如,a[]={1,2,3},所有子集是:{},{3},{2},{2,3},{1},{1,3},{1,2},{1,2,3}(输出顺序无关)。

求解过程和结果如下图所示:

1.

递归解决组合问题.

节点信息是集合中存在的元素.也就是我们选择的元素.

2.

n个整数的数组a,我们需要选择n次,因此i表示当前节点的位置.

递归的出口试i==n的时候.

3.

递归函数只需要做两件事情,第一件事是选择,第二件事是不选择.

只需要维护path信息即可.

 
#include <bits/stdc++.h>
using namespace std;
#define int long long
#define endl '\n'

// 定义一个全局的整数数组
vector<int> arr = {1, 2, 3};
// 定义一个全局的路径向量,用于存储当前路径
vector<int> path;

// 深度优先搜索函数,用于生成所有子集
void dfs(int i) {
    // 如果当前索引等于数组大小,说明已经到达叶子节点
    if (i == arr.size()) {
        cout << "{ ";
        // 输出当前路径中的元素,即一个子集
        for (const auto& x : path) {
            cout << x << " ";
        }
        cout << "}" << endl;
        return;
    }
    // 选择当前元素
    path.push_back(arr[i]);
    // 递归调用,处理下一个元素
    dfs(i + 1);
    // 撤销选择,回溯到上一个状态
    path.pop_back();
    // 不选择当前元素
    dfs(i + 1);
}

// 初始化函数(在这个程序中没有实际内容)
void init() {}

// 主解决函数,调用深度优先搜索函数
void solve() {
    dfs(0);
}

// 主函数,程序的入口
signed main() {
    ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
    init(); // 初始化
    solve(); // 解决问题
}

使用回溯法求解“和”问题

设计一个算法在1,2,…,9(顺序不能变)数字之间插入+或-或什么都不插入,使得计算结果总是100的程序,并输出所有的可能性。

例如:1+2+34–5+67–8+9=100。

1.

1~9中间有8个可以选择的符号,所以我们需要dfs8次,每一次对一个位置进行选择.

选择+或者-或者空.

2.

递归的节点信息,i表示当前的节点位置.

sum表示当前累加和.

lastnums表示最后一个进来的数字.

path表示当前表达式.

3.

选择空的时候,维护path只需要尾插当前数字.

维护sum只需要剪掉最后一个进来的数字,然后将新数字添加进来.

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

// 初始化表达式路径为 "1"
string path = "1"; 

// 深度优先搜索函数
void dfs(int i, int sum, int lastnums) {
        // 如果当前索引等于10,说明已经处理完所有数字
        if (i == 10) {  
                // 如果当前累计和等于100,输出表达式
                if (sum == 100) { 
                        cout << path << "=100" << endl;
                }
                return;
        }

        // 保存当前路径
        string oldpath = path;  

        // 选择加号
        path += "+" + to_string(i);  // 在路径中添加当前数字和加号
        dfs(i + 1, sum + i, i);  // 递归处理下一个数字,更新累计和和最后一个数字
        path = oldpath;  // 恢复路径

        // 选择减号
        path += "-" + to_string(i);  // 在路径中添加当前数字和减号
        dfs(i + 1, sum - i, -i);  // 递归处理下一个数字,更新累计和和最后一个数字
        path = oldpath;  // 恢复路径

        // 选择不插入符号,直接连接数字
        path += to_string(i);  // 在路径中直接添加当前数字
        int new_lastnums = lastnums > 0 ? lastnums * 10 + i : lastnums * 10 - i;  // 更新最后一个数字
        dfs(i + 1, sum - lastnums + new_lastnums, new_lastnums);  // 递归处理下一个数字,更新累计和和最后一个数字
        path = oldpath;  // 恢复路径
}

// 初始化函数(在这个程序中没有实际内容)
void init() {}

// 主解决函数,调用深度优先搜索函数
void solve() {
        dfs(2, 1, 1);  // 从数字2开始递归,初始累加和为1,初始最后一个数字为1
}

// 主函数,程序的入口
signed main() {
        ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
        init();  // 初始化
        solve();  // 解决问题
        return 0;
}

使用回溯法求解全排列问题

有一个含n个整数的数组a,所有元素均不相同,求其所有元素的全排列。

例如,a[]={1,2,3},得到结果是(1,2,3)、(1,3,2)、(2,3,1)、(2,1,3)、(3,1,2)、(3,2,1)

求解过程和结果如下图所示:

1.

全排列问题,用递归解决.n个位置,考虑每一个位置的每一种情况.

例如第一个问题可以选择哪些数字,第二个位置可以选择哪一些数字.以此类推.

2.

当前节点信息,i表示当前处于哪一个位置.

visited存储已经使用过的数字.

ret表示全排列序列.

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

#define int long long
#define endl '\n'

// 用于存储当前排列的字符串
string ret = "";
// 定义常量n,表示数组的大小
const int n = 3;
// 用于标记每个数字是否已经被使用过
vector<bool> visited;

// 深度优先搜索函数
void dfs(int i) {
    // 如果当前索引大于n,说明已经生成了一个完整的排列
    if (i > n) {
        cout << "{ ";
        // 输出当前排列
        for (const auto& num : ret) {
            cout << num << " ";
        }
        cout << "}" << endl;
        return;
    }

    // 遍历所有可能的数字
    for (int j = 1; j <= n; j++) {
        // 如果数字j没有被使用过
        if (!visited[j]) {
            visited[j] = true;  // 标记数字j为已使用
            ret += to_string(j);  // 将数字j添加到当前排列
            dfs(i + 1);  // 递归处理下一个位置
            visited[j] = false;  // 回溯,标记数字j为未使用
            ret.pop_back();  // 回溯,移除最后一个字符
        }
    }
}

// 初始化函数,初始化visited数组
void init() {
    visited.assign(n + 1, false);  // 初始化visited数组,大小为n+1,并设置为false
}

// 主解决函数,调用深度优先搜索函数
void solve() {
    dfs(1);  // 从位置1开始生成排列
}

// 主函数,程序的入口
signed main() {
    ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
    init();  // 初始化
    solve();  // 解决问题
}

使用回溯法求解0/1背包问题(同时进行左剪枝和右剪枝改进)

有n个重量分别为{w1,w2,…,wn}的物品,它们的价值分别为{v1,v2,…,vn},给定一个容量为W的背包。设计从这些物品中选取一部分物品放入该背包的方案,每个物品要么选中要么不选中,要求选中的物品不仅能够放到背包中,而且具有最大的价值。

并对下表所示的4个物品求出W=6时的所有解和最佳解。

物品编号

重量

价值

1

5

4

2

3

4

3

2

3

4

1

1

1.

递归函数,节点信息i表示当前考虑选择或者不选择的物品下标.

w表示当前背包里面的重量.

v表示当前背包里面的价值.

path表示当前背包选择的物品.

2.

剪枝操作,如果当前背包的w重量大于最大重量,返回.

如果当前位置i位置到结尾的所有的物品的价值加上当前价值都小于等于ret的价值,此时不可能使得ret变大,所以直接返回.

 
#include <bits/stdc++.h>
using namespace std;
#define int long long
#define endl '\n'

// 物品结构体,包含重量和价值
struct node {
    int weight = 0;
    int value = 0;
};

// 结果结构体,包含路径、总重量和总价值
struct node1 {
    string path = "";
    int weight = 0;
    int value = 0;
};

// 物品列表,包含每个物品的重量和价值
vector<node> cost = { {5, 4}, {3, 4}, {2, 3}, {1, 1} };
const int n = 4; // 物品数量
node1 ret; // 最优结果
string path; // 当前路径
int count1 = 1; // 方案计数
const int maxWeight = 6; // 背包最大容量
vector<int> sum_value; // 从当前位置到末尾的价值总和

// 深度优先搜索函数,不使用剪枝
void dfs(int i, int w, int v) {
    // 如果当前索引等于物品数量,表示已经处理完所有物品
    if (i == n) {
        path += "}"; // 在路径末尾添加右花括号
        // 输出当前的路径、总重量和总价值
        printf("%-10lld%-20s%-10lld%-10lld%-10s\n", count1++, path.c_str(), w, v, w <= maxWeight ? "能" : "否");
        // 如果当前重量不超过最大重量且当前价值大于最优结果的价值,更新最优结果
        if (w <= maxWeight && ret.value < v) {
            ret.path = path;
            ret.value = v;
            ret.weight = w;
        }
        path.pop_back(); // 回溯,移除右花括号
        return;
    }
    string oldpath = path; // 记录当前路径
    // 选择当前物品
    path += to_string(i + 1) + " "; // 在路径中添加当前物品的编号
    dfs(i + 1, w + cost[i].weight, v + cost[i].value); // 递归处理下一个物品
    path = oldpath; // 回溯,恢复路径

    // 不选择当前物品
    dfs(i + 1, w, v); // 递归处理下一个物品
}

// 深度优先搜索函数,使用剪枝
void dfs1(int i, int w, int v) {
    // 剪枝条件1:当前重量超过背包容量
    if (w > maxWeight) return;
    // 剪枝条件2:当前价值加上剩余物品的最大价值不超过当前最优解
    if (v + (i < n ? sum_value[i] : 0) <= ret.value) return;

    // 如果当前索引等于物品数量,表示已经处理完所有物品
    if (i == n) {
        path += "}"; // 在路径末尾添加右花括号
        // 输出当前的路径、总重量和总价值
        printf("%-10lld%-20s%-10lld%-10lld%-10s\n", count1++, path.c_str(), w, v, w <= maxWeight ? "能" : "否");
        // 如果当前重量不超过最大重量且当前价值大于最优结果的价值,更新最优结果
        if (w <= maxWeight && ret.value < v) {
            ret.path = path;
            ret.value = v;
            ret.weight = w;
        }
        path.pop_back(); // 回溯,移除右花括号
        return;
    }
    string oldpath = path; // 记录当前路径
    // 选择当前物品
    path += to_string(i + 1) + " "; // 在路径中添加当前物品的编号
    dfs1(i + 1, w + cost[i].weight, v + cost[i].value); // 递归处理下一个物品
    path = oldpath; // 回溯,恢复路径

    // 不选择当前物品
    dfs1(i + 1, w, v); // 递归处理下一个物品
}

// 初始化函数,计算每个位置到末尾的价值总和
void init() {
    sum_value.assign(n, 0); // 初始化sum_value数组
    for (int i = n - 1; i >= 0; i--) {
        // 计算当前位置到末尾的总价值
        sum_value[i] = (i + 1 >= n ? 0 : sum_value[i + 1]) + cost[i].value;
    }
}

// 主解决函数,调用深度优先搜索函数
void solve() {
    cout << "dfs1:" << endl;
    path += "{ "; // 初始化路径
    cout << "0/1背包的求解方案" << endl;
    // 输出标题信息
    printf("%-10s%-20s%-10s%-10s%-10s\n", "序号", "选中物品", "总重量", "总价值", "能否装入");
    dfs(0, 0, 0); // 调用不剪枝的dfs函数
    // 输出不剪枝的最佳方案
    printf("最佳方案为 选中物品:%s,总重量:%lld,总价值:%lld\n\n", ret.path.c_str(), ret.weight, ret.value);

    ret = node1(); // 重置最优结果
    count1 = 1; // 重置计数

    cout << "dfs2:" << endl;
    path = "{ "; // 重新初始化路径
    cout << "0/1背包的求解方案" << endl;
    // 输出标题信息
    printf("%-10s%-20s%-10s%-10s%-10s\n", "序号", "选中物品", "总重量", "总价值", "能否装入");
    dfs1(0, 0, 0); // 调用剪枝的dfs1函数
    // 输出剪枝的最佳方案
    printf("最佳方案为 选中物品:%s,总重量:%lld,总价值:%lld\n", ret.path.c_str(), ret.weight, ret.value);
}

// 主函数,程序的入口
signed main() {
    ios::sync_with_stdio(0), cin.tie(0), cout.tie(0); // 提高输入输出效率
    init(); // 初始化
    solve(); // 解决问题
}

结尾

最后,感谢您阅读我的文章,希望这些内容能够对您有所启发和帮助。如果您有任何问题或想要分享您的观点,请随时在评论区留言。

同时,不要忘记订阅我的博客以获取更多有趣的内容。在未来的文章中,我将继续探讨这个话题的不同方面,为您呈现更多深度和见解。

谢谢您的支持,期待与您在下一篇文章中再次相遇!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

妖精七七_

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

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

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

打赏作者

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

抵扣说明:

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

余额充值