4 - 贪心算法思想的应用
删除数字问题
任务描述
给定 n 个纯数字组成的数字串,删除其中 k(k<n) 个数字后,剩下的数字按原来的秩序组成一个新的正整数,确定删除方案,使得剩下的数字组成的新的正整数最大。
贪心算法的基本概念
贪心算法又称之为贪婪算法,指的是在求解问题时,总是选择当前最好结果的方案,而不从整体考虑最优解法。贪心算法的两个基本要素分别是贪心选择和最优子结构。
贪心选择:求解问题的整体最优解可以通过一系列的局部最优的选择来实现,即贪心选择。
最优子结构:一个问题的最优解包含其子问题的最优解,称此问题具备最优子结构性质。
贪心算法的基本概念如下:
-
- 贪心算法是一种着眼局部的简单而适应范围有限的优化策略。
-
- 贪心算法在求解最优化问题时,从初始阶段开始,每一个阶段总是做一个使局部最优的贪心选择,不断把将问题转化为规模更小的子问题。也就是说贪心算法并不从整体最优考虑,每一阶段所做出的选择只是着眼于局部最优的选择。这样处理,对有些问题来说也能得到最优解,但也并不总是这样。
-
- 贪心选择性质:所谓贪心选择性质是指所求问题的整体最优解可以通过一系列局部最优的选择,即贪心选择来达到。这是一问题可用贪心算法求解的前提,也是贪心算法与动态规划算法的主要区别。
对于一个具体问题,要确定它是否具有贪心选择性质,必须证明每一步所作的贪心选择最终能达到问题的整体最优。
删除数字问题求解思路
对于删除数字问题,第一次删除一个数字所得到的最大整数是当前的最优解,然后剩下的整数继续删除一个数字,得到的最大整数仍然是当前的最优解,以此类推,每次删除数字都选择得到当前最优解的策略。因此,最终删除 k 个数字后余下的最大整数即为整体的最优解。根据贪心算法的基本步骤,求解思路如下:
-
- 设大整数数组为 a[0,1,…,n−1],每个数组元素表示大整数的一位,大整数的最高位为 a[0],最低位为 a[n−1],用赋值为−1 来表示删除操作,例如 a[1]=−1,则表示删除数组索引 1 处的数字;
-
- 求解 n 个纯数字组成的数字串删除 k 个数字后余下最大整数的问题,可以划分为 k 个子问题:删除 1 位数字后余下最大整数问题。
-
- 依次对每个子问题求解:从左往右查询(i=0→n−2),对于数组 a 中出现的第一个数据对 a[i]<a[i+1],删除 a[i] 后余下的整数一定比删除 a[i+1] 后余下的整数要大(其余数字位置不变,相连两个数字中,删除小的数字 a[i] 后余下的整数 a[0,1,…i−1,i+1,i+2,…n−1] 一定比删除大的数字 a[i+1] 余下的整数 a[0,1,…,i−1,i,i+2,…n−1] 要大,因为新的整数在第 i 个位置上的数字前者大于后者)。为了实现方便,而不需要数组移位来形成新的整数,按先前的设定,将 a[i] 赋值为−1,往后的子问题在求解时注意跳过值为−1 的数字。
-
- k 个子问题依次求解出最优解之后,得到的整数即为 n 个数字串删除 k 个数字后余下的最大整数。
上面第 3 步在求解 k 个子问题时用的是暴力尝试的方法,复杂度为 O(k∗n),为了提高算法执行效率,可以维护每个子问题求解后 i 的位置,而不是每次都从头开始,这样子的算法复杂度为 O(n+k),具体维护方法是:当 a[i]=−1 时,让 i 往左移(i=i−1),直到 i=0 或 a[i]!=−1 为止。
编程要求
本关的编程任务是补全右侧代码片段 main 中 Begin 至 End 中间的代码,具体要求如下:
在 main 中,读取大整数字符串并存入字符数组 s,然后将字符数组 s 转换到整型数组 a 中。读取整数 k(表示要删除 k 个数字),然后根据贪心策略,求解出删除 k 个数字后剩下的最大整数,并在一行输出。
测试说明
平台的测试样例:
测试输入:
79502867154829179316
8
预期输出:
987829179316
代码实现
#include <iostream>
#include <cstdio>
#include <cstring>
#include <algorithm>
using namespace std;
int main(int argc, const char * argv[]) {
char s[1001];
int a[1001];
int k; // 删除 k 个数字
int n;
// 请在这里补充代码,完成本关任务
/********* Begin *********/
//while()
cin>>s;
cin>>k;
int index = 0;
int len = strlen(s);
for(int i=0;i<len;i++){
a[i] = s[i]-'0';
}
int i;
for(int loop = 0;loop<k;loop++){
for(i = 0;i<len-1;i++){
if(a[i]<a[i+1]){
for(int k = i;k<len-1;k++){
a[k] = a[k+1];
}
i = len;
}
}
}
for(int j=0;j<len-k;j++){
cout<<a[j];
}
/********* End *********/
return 0;
}
5 - 回溯法算法思想的应用
n 位逐位整除数
任务描述
掌握回溯法算法思想,并能利用回溯法算法思想解决 n 位逐位整除数问题。
n 位逐位整除数(简称整除数):从其高位开始,高 1 位能被整数 1 整除(显然),高 2 位能被整数 2 整除,…,整个 n 位能被整数 n 整除。给定整数 n,求所有的 n 位整除数的个数。
例如,整数 102450 就是一个 6 位整除数。
相关知识
为了完成本关任务,需要掌握:
- 回溯法的基本思想
- 回溯法的基本步骤
- 回溯法的算法框架
- 整除数的求解思路
回溯法的基本思想
有许多问题,当需要找出它的解集或者要求回答什么解是满足某些约束条件的最佳解时,往往要使用回溯法。
回溯法的基本做法是搜索,或是一种组织得井井有条的,能避免不必要搜索的穷举式搜索法。这种方法适用于解一些组合数相当大的问题。
回溯法在问题的解空间树中,按深度优先策略,从根结点出发搜索解空间树。算法搜索至解空间树的任意一点时,先判断该结点是否包含问题的解。如果包含,进入该子树,继续按深度优先策略搜索。如果肯定不包含,则跳过对该结点为根的子树的搜索,逐层向其祖先结点回溯。
回溯法的基本步骤
根据回溯法的基本思想,可以得到回溯法的基本步骤如下:
- 针对所给问题,定义问题的解空间;
- 确定易于搜索的解空间结构;
- 以深度优先方式搜索解空间,并在搜索过程中用剪枝函数避免无效搜索。
其中,两个常用的剪枝函数为:
- 约数函数:在扩展结点处减去不满足约束的子树 ;
- 限界函数:减去得不到最优解的子树。
对于问题的一个实例,解向量满足显式约束条件的所有多元组,构成了该实例的一个解空间:
- 问题的解向量:回溯法希望一个问题的解能够表示成一个 n 元式 ( x 1 , x 2 , . . . , x n ) (x_1,x_2,...,x_n) (x1,x2,...,xn)的形式
- 显约束:对分量 x i x_i xi 的取值限定
- 隐约束:为满足问题的解而对不同分量之间施加的约束(通常用于剪枝)
回溯法的算法框架
回溯法对解空间树作深度优先搜索,因此,在一般情况下用递归方法实现回溯法。
具体算法框架及其伪代码如下:
void backt\frack (int t)//按深度优先从t层推进到第t+1层
{
if (t > n){
output (x); // 到达叶子结点,将计算或结果输出
}
else {
for (int i = f(n,t); i <= g(n,t); i ++ ){// 遍历结点t的所有子结点
x[t] = h[i];
if (constraint (t) && bound (t)){
backt\frack (t + 1);//如果不满足剪枝条件继续遍历
}
//回溯到该层的其他结点,然后继续搜索解空间
}
}
}
整除数的求解思路
n 位整除数的解法可以暴力枚举每一位数字,然后通过试除法进行验证,显然,这样的方法复杂度非常高,而且非常多的无效组合。运用回溯法算法可以很好的避免无效的组合,提高检索效率。根据回溯法的基本步骤,可得到如下回溯解法:
- 针对 n 位整除数问题,它的解空间为:第 1 位可能的数字为 1,2,…9,第 2 至第 n 位可能的数字都为 0,1,…9,因此共 9 × 1 0 n − 1 9×10^{n-1} 9×10n−1个可能的解;
- 解空间结构:用数组 a[1,2,…n] 表示整除数问题的一个解,其中 a[1]!=0,求解时按索引 t=1 开始,依次递归得到数组 a;
- 以深度优先的方式搜索解空间,若当前的整数 a[1,2,…t] 不能整除整数 t,则剪枝。
例如 n=6 时,回溯法的过程如下图所示。t=1,候选数字为 0,1,2,…9,其中 0 是不符合整数第 1 位不为 0 的规则,设置当前的数字为 a[t=1]=1;递归,t=t+1=2,数字 0,2,4,6,8 与 a[1] 所组合的整数 a[1,2] 是能被 t=2 整除的,设置当前的数字为 a[t=2]=0;以此类推,直到 t=7 时,超过了指定的长度 n=6,当前递归搜索结束,整除数个数加 1。然后回溯到 t=6 时其它的合法数字,继续递归搜索。再然后回溯到 t=5 时其他的合法数字,继续递归搜索。以此类推,完成整个解空间的搜索。
编程要求
本关的编程任务是补全右侧代码片段 backt\frack
中 Begin
至 End
中间的代码,具体要求如下:
- 在
backt\frack
中,根据回溯法算法的思想,遍历和统计 n 位逐位整除数的个数。 其中,参数数组 a 记录满足条件的整除数(整除数长度 n 超过 int64 位存储范围,所以用整型数组模拟存储),参数 t(初始为 1)表示当前整除数长度(t<=n
),参数 n 表示待查询的整除数长度,取地址引用的参数sum
表示整除数的个数。
测试说明
平台的测试样例:
测试输入:6
预期输出:1200
解题思路
代码实现
//
// main.cpp
// step1
//
// Created by ljpc on 2018/12/8.
// Copyright © 2018年 ljpc. All rights reserved.
//
#include <iostream>
#include <algorithm>
#include <cstdio>
using namespace std;
void backtrack(int* a, int t, int n, int& sum)
{
// n 位数
// t = 1~n
// 请在这里补充代码,完成本关任务
/********* Begin *********/
if (t == n+1) {
sum++;
return;
}
else {
for (int i = t; i <= n; i++) { // 处理每一位
if (i == 1) {
for (int j = 1; j <= 9; j++) {
a[i] = j;
int num = 0;
for (int b = 1; b <= i; b++) {
int num10 = 1;
for (int d = b; d < i; d++) {
num10 *= 10;
}
num += a[b] * num10;
}
// cout << "num : " << num << endl;
backtrack(a, t + 1, n, sum);
}
}
else {
for (int j = 0; j <= 9; j++) {
a[i] = j;
long long int num = 0;
for (int b = 1; b <= i; b++) {
int num10 = 1;
for (int d = b; d < i; d++) {
num10 *= 10;
}
num += a[b] * num10;
}
int yu = 0;
for (int p = 1; p <= t; p++) {
yu = (yu * 10 + a[p]) % t;
}
if (yu == 0) {
backtrack(a, t + 1, n, sum);
}
else {
continue;
}
}
}
return;
}
}
/********* End *********/
}
int main(int argc, const char * argv[]) {
int a[101];
int n;
scanf("%d", &n); // 验证位数
int sum = 0;
backtrack(a, 1, n, sum);
printf("%d\n", sum);
return 0;
}