算法分析与设计「一」枚举

一、枚举算法思想


什么是枚举算法?

在进行归纳推理时,如果逐个考察了某类事件的所有可能情况,因而得出一般结论,那么这结论是可靠的,这种归纳方法叫做 枚举法。也就是说枚举是基于 逐个尝试答案 的一种问题求解策略。

枚举法的特点

枚举算法因为要列举问题的所有可能的答案,所以它具备以下几个特点:

  1. 得到的结果肯定是正确的。
  2. 可能做了很多的无用功,浪费了宝贵的时间,效率低下。
  3. 通常会涉及到求极值(如最大,最小,最重等)。
  4. 数据量大的话,可能会造成时间崩溃。

枚举法基本思路

采用枚举算法解题的基本思路:

  1. 确定枚举对象、枚举范围和判定条件;
  2. 枚举可能的解,验证是否是问题的解。
     

二、例题分析


例题一:完美立方

在这里插入图片描述

要求:请按照 a 的值,从小到大依次输出。当两个完美立方等式中a的值相同,则 b 值小的优先输出、仍相同则 c 值小的优先输出、再相同则 d 值小的先输出。

	// 输入
	24
	// 输出
	Cube = 6, Triple = (3,4,5)
	Cube = 12, Triple = (6,8,10)
	Cube = 18, Triple = (2,12,16)
	Cube = 18, Triple = (9,12,15)
	Cube = 19, Triple = (3,10,18)
	Cube = 20, Triple = (7,14,17)
	Cube = 24, Triple = (12,16,20)

解题思路:四重循环枚举 a、b、c、d,a 在最外层,d 在最里层,每一层都是从小到大枚举,且每个变量枚举范围如下:

  1. a 范围 [2, N]
  2. b 范围 [2, a-1]
  3. c 范围 [b, a-1]
  4. d 范围 [c, a-1]

代码实现:

    int N;
    cin >> N;
    for (int a = 2; a <= N; ++a)
    {
        for (int b = 2; b < a; ++b)
        {
            for (int c = b; c < a; ++c)
            {
                for (int d = c; d < a; ++d)
                {
                    if (a * a * a == b * b * b + c * c * c + d * d * d)
                        cout << "Cube = " << d << ", Triple = (" << b << "," << c << "," << d << ")" << endl;
                }
            }
        }
    }

注意:可以通过合理控制枚举范围,来提高运算效率。
 
例题二:假币问题

题目描述:有 12 枚硬币。其中有 11 枚真币和 1 枚假币。假币和真币重量不同,但不知道假币比真币轻还是重。现在,用一架天平称了这些币三次,告诉你称的结果,请你找出假币并且确定假币是轻是重(数据保证一定能找出来)。

输入: 输入第一行是测试数据组数。每组数据有三行,每行表示一次称量的结果。银币标号为 A - L 。每次称量的结果用三个以空格隔开的字符串表示:天平左边放置的硬币 天平右边放置的硬币 平衡状态。其中平衡状态用 updowneven 表示,分别为 右端高右端低平衡。天平左右的硬币数总是相等的。

	// 输入样例
	1
	ABCD EFGH even
	ABCI EFJK up
	ABIJ EFGH even
	// 输出样例
	K is the counterfeit coin and it is light. 

解题思路:对于每一枚硬币先假设它是轻的,看这样是否符合称量结果。如果符合,问题即解决。如果不符合,就假设它是重的,看是否符合称量结果。把所有硬币都试一遍,一定能找到特殊硬币。
具体来说,设置一个 IsFake() 函数,我们先根据假币是轻的情况,来进行向下讨论。如果天平的右侧翘起 result[i][0] = 'u',则说明右端存在假币,因此如果判断到右侧没有假币 strchr(pRight, c) == NULL 则返回 false。同理,当两侧平衡时,判断左右两侧只要有一侧有假币,则返回 false;左侧翘起,判断左侧没有假币则返回 false。这是假币为轻的情况,如果假币为重时,只需要将左右指针指向交换,就变成了前一种情况。

实现代码:

#include <iostream>
#include <cstdio>
#include <cstring>
using namespace std;

char Left[3][7];   // 天平坐边硬币
char Right[3][7];  // 天平右端硬币
char result[3][7]; // 结果

bool IsFake(char c, bool light) // c 为假币,同时 light 为 true 表示假设假币为轻
{
    // 三次称量结果不矛盾,假设成立
    for (int i = 0; i < 3; ++i)
    {
        char *pLeft, *pRight; // 指向天平两端的字符串
        if (light)
        {
            pLeft = Left[i];
            pRight = Right[i];
        }
        else
        { // 假设硬币为重的,则把左右称量结果互换
            pLeft = Right[i];
            pRight = Left[i];
        };
        switch (result[i][0]) // 天平右边的情况
        {
        case 'u':
            if (strchr(pRight, c) == NULL)
                return false;
            break;
        case 'e':
            if (strchr(pLeft, c) || strchr(pRight, c))
                return false;
            break;
        case 'd':
            if (strchr(pLeft, c) == NULL)
                return false;
            break;
        }
    }
    return true;
}

int main()
{
    int t;
    cin >> t;

    while (t--)
    {
        for (int i = 0; i < 3; ++i)
            cin >> Left[i] >> Right[i] >> result[i];
        for (char c = 'A'; c < 'L'; ++c)
        {
            if (IsFake(c, true))
            {
                cout << c << " is the counterfeit coin and it is light." << endl;
                break;
            }
            else if (IsFake(c, false))
            {
                cout << c << " is the counterfeit coin and it is heavy." << endl;
                break;
            }
        }
    }

    return 0;
}

strchr() 方法

  • 库:c 标准库 <string.h>
  • 语法: char *strchr(const char *str, char c)
  • 功能:获取字符串 str 中字符 c 第一次出现的位置。
  • 返回值:返回在字符串 str 中第一次出现字符 c 的位置,如果未找到该字符则返回 NULL。

例题三:熄灯问题

题目概述:有一个由按钮组成的矩阵,其中每行有 6 个按钮,共 5 行。每个按钮的位置上有一盏灯,当按下一个按钮后, 该按钮以及周围位置(上边,下边,左边,右边)的灯都会改变状态:

Ⅰ. 如果灯原来是点亮的,就会被熄灭。

Ⅱ. 如果灯原来是熄灭的,则会被点亮。

  • 在矩阵角上的按钮改变 3 盏灯的状态
  • 在矩阵边上的按钮改变 4 盏灯的状态
  • 其他的按钮改变 5 盏灯的状态
    在这里插入图片描述

Ⅲ. 与一盏灯毗邻的多个按钮被按下时,一个操作会抵消另一次操作的结果。

要求:给定矩阵中每盏灯的初始状态,求一种按按钮方案,使得所有的灯都熄灭。

输入: 第一行是一个正整数 N,表示需要解决的案例数。每个案例由 5 行组成, 每一行包括 6 个数字,这些数字以空格隔开, 可以是 0 或 1。0 表示灯的初始状态是 熄灭 的,1 表示灯的初始状态是 点亮 的。

输出:对每个案例,首先输出一行:输出字符串 PUZZLE #m, 其中 m 是该案例的序号。接着按照该案例的输入格式输出 5 行,1 表示需要把对应的按钮按下,0 表示不需要按对应的按钮。每个数字以一个空格隔开。

	// 输入
	2
	0 1 1 0 1 0
	1 0 0 1 1 1
	0 0 1 0 0 1
	1 0 0 1 0 1
	0 1 1 1 0 0
	0 0 1 0 1 0
	1 0 1 0 1 1
	0 0 1 0 1 1
	1 0 1 1 0 0
	0 1 0 1 0 0 
	// 输出
	PUZZLE #1
	1 0 1 0 0 1
	1 1 0 1 0 1
	0 0 1 0 1 1
	1 0 0 1 0 0
	0 1 0 0 0 0
	PUZZLE #2
	1 0 0 1 1 1
	1 1 0 0 0 0
	0 0 0 1 0 0
	1 1 0 1 0 1
	1 0 1 1 0 1

解题思路:

  1. 首先,你可能会想到暴力求解这个问题,也就利用枚举。枚举所有可能的按钮(开关)状态,对每个状态计算一下最后灯的情况,看是否都熄灭。但是这种枚举的状态数是 2^30 ,会导致超时。那么怎么减少枚举状态数呢?
  2. 基本思路是如果存在某个 局部,一旦这个局部的状态被确定,那么剩余其他部分的状态只能是确定的一种,或者不多的 n 种,那么就只需枚举这个局部的状态即可。举个例子来说,第一行的开关状态确定了,就只能通过调节第二行开关来使其变化,调节其他行不会对第一行产生影响。因此,第一行开关状态确定了,第二行状态也就确定了,以此类推,后面各行状态都确定了。推算出最后一行的开关状态,然后看看最后一行的开关起作用后是否所有灯全都熄灭。这时只需枚举第 1 行的状态,状态数是 2^6 = 64
  3. 此外,因为这里的矩阵元素为 0、1,考虑到时间和空间复杂度,可以采用 位运算 。这里只需开一个一维的 char 类型的数组,即可将矩阵存储下来。(因为 char 类型数组有 8 比特,而矩阵中一行元素只有 6 个,因此 char 数组中一个元素即可对应矩阵中的一行)。

位运算相关文章: 位运算(&、|、^、~、>>、<<)【技巧总结】位运算装逼指南

实现代码:这题也太难为我了。。。勉强看懂,但是让我自己实现的话,😅

#include <iostream>
#include <cstdio>
#include <cstring>
using namespace std;

char oriLights[5]; // 初始灯矩阵
char lights[5];    // 变化过程中的灯矩阵
char result[5];    // 结果开关矩阵

// 获取第 i 位
int GetBit(char c, int i)
{
    // 101001 获取第 4 位
    // 1. 000010 & 000001 = 000000
    // 2. 即获取到 101001 的第四位 0
    return (c >> i) & 1;
}

// 设置第 i 位为 v (0 或 1)
void SetBit(char &c, int i, int v)
{
    if (v)
        // 设置第 i 位为 1
        // c: 101001   i: 4   1<<i: 010000
        // 101001 | 010000 = 111001 成功将第 i 位设置为 1
        c |= (1 << i);
    else
        // 设置第 i 位为 0
        // c: 101001   i: 4   1<<i: 010000
        // 101001 & 101111 = 101001 成功将第 i 位设置为 0
        c &= ~(1 << i);
}

// 反转第 i 位
void FlipBit(char &c, int i)
{
    // 反转第 i 位
    // c: 101001   i: 4   1<<i: 010000
    // 101001 ^ 010000 = 111001 成功将第 i 位反转
    c ^= (1 << i);
}

void OutputResult(int t, char result[])
{
    cout << "PUZZLE #" << t << endl;
    for (int i = 0; i < 5; ++i)
    {
        for (int j = 0; j < 6; ++j)
        {
            cout << GetBit(result[i], j);
            if (j < 5)
                cout << " ";
        }
        cout << endl;
    }
}

int main()
{
    int T;
    cin >> T;
    for (int t = 1; t <= T; ++t)
    {
        memset(oriLights, 0, sizeof(oriLights));
        // 读入初始灯状态
        for (int i = 0; i < 5; ++i)
        {
            for (int j = 0; j < 6; ++j)
            {
                int s;
                cin >> s;
                SetBit(oriLights[i], j, s);
            }
        }
        // 枚举第一行开关所有可能的 64 种状态
        for (int n = 0; n < 64; ++n)
        {
            int switches = n; // 当前行的开关状态
            memcpy(lights, oriLights, sizeof(oriLights));
            for (int i = 0; i < 5; ++i)
            {
                result[i] = switches; // 第 i 行的开关状态
                for (int j = 0; j < 6; ++j)
                {
                    // (i, j) 按下时 进行处理
                    if (GetBit(switches, j))
                    {
                        if (j > 0)
                            FlipBit(lights[i], j - 1); // 反转左侧灯
                        FlipBit(lights[i], j);         // 反转当前开关位置灯
                        if (j < 5)
                            FlipBit(lights[i], j + 1); // 反转右侧灯
                    }
                }
                // 处理 i+1 行
                if (i < 4)
                    lights[i + 1] ^= switches; // 改下一行的灯
                switches = lights[i];
            }
            if (lights[4] == 0)
            {
                OutputResult(t, result);
                break;
            }
        }
    }

    return 0;
}

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 3
    评论
专题十:算法分析设计 1.常用的算法设计方法:   1.1 迭代法   1.2 穷举搜索法   1.3 递推法   1.4 递归法   1.5 贪婪法   1.6 分治法   1.7 动态规划法   1.8 回溯法 算法基础部分: 算法是对特定问题求解步骤的一种描述,算法是指令的有限序列,其中每一条指令表示一个或多个操作。 算法具有以下5个属性:   有穷性:一个算法必须总是在执行有穷步之后结束,且每一步都在有穷时间内完成。   确定性:算法中每一条指令必须有确切的含义。不存在二义性。只有一个入口和一个出口   可行性:一个算法是可行的就是算法描述的操作是可以通过已经实现的基本运算执行有限次来实现的。   输入:一个算法有零个或多个输入,这些输入取自于某个特定对象的集合。   输出:一个算法有一个或多个输出,这些输出同输入有着某些特定关系的量。 所以对应的算法设计的要求:   正确性:算法应满足具体问题的需求;   可读性:算法应该好读,以有利于读者对程序的理解;   健壮性:算法应具有容错处理,当输入为非法数据时,算法应对其作出反应,而不是产生莫名其妙的输出结果。   效率与存储量需求:效率指的是算法执行的时间;存储量需求指算法执行过程中所需要的最大存储空间。一般这两者与问题的规模有关。 1.1 迭代法: 迭代法是用于求方程或方程组近似根的一种常用的算法设计方法。设方程为f(x)=0,用某种数学方法导出等价的形式x=g(x),然后按以下步骤执行: (1)选一个方程的近似根,赋给变量x0; (2)将x0的值保存于变量x1,然后计算g(x1),并将结果存于变量x0; (3)当x0与x1的差的绝对值还小于指定的精度要求时,重复步骤(2)的计算。 若方程有根,并且用上述方法计算出来的近似根序列收敛,则按上述方法求得的x0就认为是方程的根。上述算法用C程序的形式表示为: 【算法】迭代法求方程的根 { x0=初始近似根; do { x1=x0; x0=g(x1); /*按特定的方程计算新的近似根*/ } while ( fabs(x0-x1)>Epsilon); printf(“方程的近似根是%f\n”,x0); } 迭代算法也常用于求方程组的根,令 X=(x0,x1,…,xn-1) 设方程组为: xi=gi(X) (I=0,1,…,n-1) 则求方程组根的迭代算法可描述如下: 【算法】迭代法求方程组的根 { for (i=0;i<n;i++) x[i]=初始近似根; do { for (i=0;i<n;i++) y[i]=x[i]; for (i=0;i<n;i++) x[i]=gi(X); for (delta=0.0,i=0;i<n;i++) if (fabs(y[i]-x[i])>delta) delta=fabs(y[i]-x[i]); } while (delta>Epsilon); for (i=0;i<n;i++) printf(“变量x[%d]的近似根是 %f”,I,x[i]); printf(“\n”); } 具体使用迭代法求根时应注意以下两种可能发生的情况: (1)如果方程无解,算法求出的近似根序列就不会收敛,迭代过程会变成死循环,因此在使用迭代算法前应先考察方程是否有解,并在程序中对迭代的次数给予限制; (2)方程虽然有解,但迭代公式选择不当,或迭代的初始近似根选择不合理,也会导致迭代失败。 1.2 穷举搜索法: 穷举搜索法是对可能是解的众多候选解按某种顺序进行逐一枚举和检验,并从中找出那些符合要求的候选解作为问题的解。 要解决的问题只有有限种可能,在没有更好算法时总可以用穷举搜索的办法解决,即逐个的检查所有可能的情况。可以想象,情况较多时这种方法极为费时。实际上并不需要机械的检查每一种情况,常常是可以提前判断出某些情况不可能取到最优解,从而可以提前舍弃这些情况。这样也是隐含的检查了所有可能的情况,既减少了搜索量,又保证了不漏掉最优解。 【问题】 将A、B、C、D、E、F这六个变量排成如图所示的三角形,这六个变量分别取[1,6]上的整数,且均不相同。求使三角形三条边上的变量之和相等的全部解。如图就是一个解。 程序引入变量a、b、c、d、e、f,并让它们分别顺序取1至6的整数,在它们互不相同的条件下,测试由它们排成的如图所示的三角形三条边上的变量之和是否相等,如相等即为一种满足要求的排列,把它们输出。当这些变量取尽所有的组合后,程序就可得到全部可能的解。细节见下面的程序。 # include <stdio.h> void main() { int a,b,c,d,e,f; for (a=1;a<=6;a++) //a,b,c,d,e依次取不同的值 for (b=1;b<=6;b++) { if (b==a) continue; for (c=1;c<=6;c++) { if (c==a)||(c==b) continue; for (d=1;d<=6;d++) { if (d==a)||(d==b)||(d==c) continue; for (e=1;e<=6;e++) { if (e==a)||(e==b)||(e==c)||(e==d) continue; f=21-(a+b+c+d+e);//最后一个用减法算 if ((a+b+c==c+d+e))&&(a+b+c==e+f+a)) { printf(“%6d,a); printf(“%4d%4d”,b,f); printf(“%2d%4d%4d”,c,d,e); scanf(“%c”); } } } } } }

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

imByte

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

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

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

打赏作者

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

抵扣说明:

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

余额充值