第一周:枚举

回眸只為那壹抹淺笑 提交于 2020-03-17 03:40:22

枚举:基于逐个尝试答案的一种求解策略

例题1:完美立方

描述:
形如(a^3) = (b^3) +(c^3) +(d^3) 等式称为完美立方等式。例如12,6,8,10。编写一个程序,对任给的正整数N(N<=100),寻找所有的四元组(a,b,c,d),使得满足(a^3) = (b^3) +(c^3) +(d^3) ,其中a,b,c,d大于1,小于等于N,且b<=c<=d。

输入:
一个正整数N(N<=100)

输出:
每行输出一个完美立方。

输入样例:

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在最里层,每一层都是从小到大枚举,范围:a[2,N],b[2,a-1],c[b,a-1],d[c,a-1]

代码:

#include <iostream>
#include <cstdio>//通过使用c标准输入和输出库,使得输入和输出操作也可以在C++实现
using namespace std;
int main()
{
	int N,a,b,c,d;
    cin >> N;
	  for(a = 2; a <= N; a++)
	    for(b = 2; b <= a - 1; b++)
	      for(c = 2; c <= a - 1; c++)
	        for(d = 2; d <= a - 1; d++)
	        {
	        	if(N * N * N == a * a * a + b * b * b + c * c * c + d * d * d)	  
              cout << "Cube = "<< N << "Triple = ( "<< a << "," << b << "," << d << ")" << endl;
			}
	return 0;
 }  

总结:

  1. 申清题意
    题目让输入一个正整数,没有让自己判断
  2. 注意输出内容的处理
    cout<<“Cube= “<< N << “Triple = ( " << a <<”,”<< b <<”,"<< d << “)” << endl
    注意空格,变量等

例题2:生理周期

描述:
人有体力,情商,智商的高峰日子,他们分别每隔23天,28天,33天出现一次。对于每个人,我们想知道何时三个高峰落在同一天。给定三个高峰出现的日子p,e,和i(不一定是第一次高峰出现的日子),在给定另一个制定的日子d,你的任务是输出日子d之后,下一次三个高峰落在同一天的日子(用距离d的天数表示)。例如,给定日子为10,下次出现三个高峰同一天的日子是12,则输出2。

输入:
输入四个整数:p,e,i和d。p,e,i分别表示体力,情感,智力高峰出现的日子。d是给定的日子,可能小于p,e或i。所有给定日子是非负的并且小于或等于365,所求的日子小于或等于21252。

输出:
从给定日子起,下一次三个高峰同一天的日子。

输入样例:

0 0 0 0
0 0 0 100
5 20 34 325
4 5 6 7
283 102 23 320
203 301 203 40
-1 -1 -1 -1

输出样例:

Case 1:the next triple peak occurs in 21252 days.
Case 2:the next triple peak occurs in 21152 days.
Case 3:the next triple peak occurs in 19575 days.
Case 4:the next triple peak occurs in 16994 days.
Case 5:the next triple peak occurs in 8190 days.
Case 6:the next triple peak occurs in 10789 days. 

解题思路:
3. 从d+1天开始,一直试到第21252天,对其中每个日期D,看是否满足 (D-p)%23= =0 && (D-e)%28= =0 && (D-i)%33= =0
4. 跳着试

代码:

#include <iostream>
#include <cstdio>
using namespace std;
int main()
{
  int p,e,i,d, caseNo = 0;
  while(cin >> p >> e >> i >> d && p != -1) {
    int D;
    ++caseNo;
    for(D = d;(D - p) % 23; D++);
    for(;(D - e) % 28;D =+ 23 * 28);
    for(;(D - i) % 33;D =+ 23 * 28 * 33);
    cout << "Case " << caseNo << ": the next triple peak occurs in" << D-d << "days.";
  }
  return 0;
}

总结:

  1. 巧用for循环语句
    1)for(D=d;(D-p)%23;D++); 由于值唯一,语句可直接得出答案进行下面的循环
    2)for循环中间语句为判断语句,可以写为(D-p)%23,为真继续循环
  2. 整合语句,使程序简单易读
    1)for循环使用
    2)while ( cin >> p >> e >> i >> d && p != -1)

例题3:称硬币

描述:
有12枚硬币。其中有11枚真币和1枚假币。假币和真币重量不同,但不知道真币重还是假币重。现在,用一架天平称了这些币三次,告诉你称的结果,请你找出假币并确定假币的重量是轻是重。
输入样例:

1
ABCD EFGH even
ABCI EFJK up
ABIJ EFGH even 

输出样例:

K is the counterfeit coin and it is light.   

解题思路:
对于每一枚硬币先假设它是轻的,看这样是否符合称量结果。如果符合,问题即解决。如果不符合,就假设它是重的,看是否符合称量结果。把所有硬币都试一遍,一定能找到特殊硬币。

代码:

#include <iostream>
#include <cstring>
using namespace std;
char Left[3][10];
char Right[3][10];
char result[3][10];
bool IsFake(char c, bool light);
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.\n";    
          break;      
        }
        else if(IsFake(c,false)) {
          cout << c <<" is a counterfeit coin and it is heavy.\n";
          break;
        }
      }
    }
  return 0;
}
bool IsFake(char c, bool light)
{
  for(int i = 0; i < 3; i++) {
    char *pRight, *pLeft;
    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 'd':
        if(strchr(pLeft,c) == NULL)
          return false;
        break;
      
      case 'e':
        if(strchr(pLeft,c) || strchr(pRight,c))
          return false;
        break; 
    }
  }
  return true;
}

总结:

  1. 注意循环的包含问题
    for(int i=0; i<3;i++)语句仅负责打印数组,不包含剩下语句
  2. 不要有逻辑性错误
    case ‘u’,‘e’,'d’的内容
  3. 审题不清
    1)由于只有一枚假币,所以主函数中判断IsFake语句只要一次为真即可break
    2)在IsFake函数中,for(int i = 0; i < 3; i++) 负责判断排查三种情况中的硬币,又因为只有一枚假币,全部检查一遍才能获得答案,所以return true 语句的位置应该在for(int i = 0; i < 3; i++) 循环外面。
  4. 语法问题
    1)case ''语句无需大括号
    2)strchr(char *str,char character)
    参数说明:str为一个字符串的指针,character为一个待查找字符。
    所在库名:#include <string.h>
    函数功能:从字符串str中寻找字符character第一次出现的位置。
    返回说明:返回指向第一次出现字符character位置的指针,如果没找到则返回NULL。
  5. 语句整合
    while(t–)

例题4:熄灯问题

描述:
— 有一个由按钮组成的矩阵,其中每行有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

解题思路:
想法一:
枚举所有可能的按钮(开关)状态,对每个状态计算一下最后灯的情况,看是否会熄灭。
—— 每个按钮有两种状态(按下或不按下)
—— 一共有30个开关,那么状态数是2^30,太多,会超时
想法二:
存在局部,一旦局部的状态被确定,那么剩余其他部分的状态就只能是确定的一种。

代码:

#include <iostream>
#include <string>
#include <cstring>
#include <memory>
using namespace std;
char oriLights[7];
char Lights[7];
char result[7];
int GetBit(char c, int i)
{
  return c& (1 << i);
}
void SetBit(char &c, int i, int v)
{
  if(v)
    c |= (1 << i);
  else 
    c &= ~ (1 << i);
}
void FlipBit(char &c, int 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 = 0; t < T; t++) {
    for(int i = 0; i < 5; i++) {
      for(int j = 0; j < 6; j++) {
        int s;
        cin >> s;
        SetBit(oriLights[i], j, s);
      }
    }
  for(int n=0; n<64; n++) {
    int switchs=n;
    memcpy(Lights,oriLights,sizeof(oriLights));
    for(int i = 0; i < 5; i++) {
      result[i]=switchs;
      for(int j = 0; j < 6; j++) {
        if(GetBit(switchs, j)) {
          if(j > 0)
            FlipBit(Lights[i], j-1);
           FlipBit(Lights[i], j);
           if(j < 5)
             FlipBit(Lights[i], j+1);
        }
      }
      if(i < 4)
        Lights[i+1] ^= switchs;
      switchs = Lights[i];
  }
  if(Lights[4] == 0) {
    OutputResult(t, result);
    break;
  }
  }
}
return 0;
}

总结:

  1. 数列问题
    1)OutputResult函数中对于换行,空格的判断语句
    2)main函数中使用FlipBit函数对于bit所处位置的判断
  2. 题目理解
    1)上一行的状态决定下一行的开关灯选择,下一行的开关灯选择改变上一行的状态。
    2)for循环的终止条件:最后一行全部关灯
  3. 位运算
    1)^(异或): 和1异或1变0,0变1
    2)&(与):有0为0
    3)|(或):有1为1
    4)~(取反):1变0,0变1

课后习题1:特殊密码锁

描述:
有一种特殊的二进制密码锁,由n个相连的按钮组成(n<30),按钮有凹/凸两种状态,用手按按钮会改变其状态。然而让人头疼的是,当你按一个按钮时,跟它相邻的两个按钮状态也会反转。当然,如果你按的是最左或者最右边的按钮,该按钮只会影响到跟它相邻的一个按钮。当前密码锁状态已知,需要解决的问题是,你至少需要按多少次按钮,才能将密码锁转变为所期望的目标状态。

输入:
两行,给出两个由0、1组成的等长字符串,表示当前/目标密码锁状态,其中0代表凹,1代表凸。

输出:
至少需要进行的按按钮操作次数,如果无法实现转变,则输出impossible。

输入样例:

011
000

输出样例:

1

解题思路:
1.n<30,全部枚举空间约为536,870,912种,无法在规定短时间内验证完毕。
2.除了第一个和最后一个按钮,按下中间的任何一个按钮,均会改变相邻两个按钮状态,因此如果某一位置按钮与目标密码锁状态不同,前一位置状态已经相同,则均不得按下此位置按钮,可以通过按下此位置的后一位置按钮来达到目的。
3.第一个按钮是否按下,直接决定了整个密码锁后续按钮是否需要按下。枚举空间为2,而不是2^29。

代码:

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

int oriLock;
int lock;
int destLock;

inline int GetBit(int n,int i)
{
	return (n >> i) & 1;
} 

inline void SetBit(int & n,int i,int v)
{
	if(v) 
		n |= (1 << i);
	else
		n &= ~(1 << i);
}

inline void FlipBit(int & n,int i)
{
	n ^= (1 << i);
}

int main()
{
	char line[40];
	destLock = lock = oriLock = 0;
	cin >> line;
	int N = strlen(line);//头文件<cstring>的函数
	for(int i = 0; i < N; ++i)
		SetBit(oriLock,i, line[i] - '0');//和GetBit用途无区别,多设置一个变量line方便计算密码锁个数
	cin >> line;
	for(int i = 0; line[i]; ++i)
		SetBit(destLock,i, line[i] - '0');
	int minTimes = 1 << 30;//注意放在循环外
	for(int p = 0; p < 2; ++p) { 
		lock = oriLock;
		int times = 0;//放在该循环里可以验证两次最小时间
		int curButton = p;
		for(int i = 0; i < N; ++i) {
			if(curButton) {
				++ times;
				if( i > 0)
					FlipBit(lock,i-1);
				FlipBit(lock,i);
				if( i < N-1)
					FlipBit(lock,i+1);
			}
			if( GetBit(lock,i) != GetBit(destLock,i))  
				curButton = 1;
			else 
				curButton = 0;
		}
		if( lock == destLock)//选用int型变量的优势
			minTimes = min(minTimes ,times);//头文件<algorithm>的函数;记录minTimes的值而不是break
	}
	if( minTimes == 1 << 30)
		cout <<"impossible" << endl;
	else
		cout << minTimes << endl;
	return 0;
}

总结:

  1. 语法问题
    1)已知 char line = 1,则 int v= line-‘1’= 0; int t= line-1 = 48
    2)strlen()计算字符串长度,包含在cstring头文件里
    3)左移乘以2,右移除以2
    4)inline函数:为了解决一些频繁调用的小函数大量消耗栈空间的问题。但是只适合函数体内部代码简单的函数使用,不能包含复杂的结构控制语句例如while、switch,并且内联函数本身不能是直接递归函数。
    5)在对数组名称使用 == 运算符时,运算符会比较数组的开始内存地址,而不是数组的内容。要比较两个数组的内容,则必须比较它们各自的元素。
  2. 逻辑问题
    1)注意times,minTimes变量定义位置。
    2)minTimes=1<<30原因是每个密码变换,两种方式,共计2*30=60个单位时间。

课后习题2:拨钟问题

描述:
有9个时钟,排成一个3*3的矩阵。

|-------|    |-------|    |-------|
|       |    |       |    |   |   |
|---O   |    |---O   |    |   O   |
|       |    |       |    |       |
|-------|    |-------|    |-------|
    A            B            C    
|-------|    |-------|    |-------|
|       |    |       |    |       |
|   O   |    |   O   |    |   O   |
|   |   |    |   |   |    |   |   |
|-------|    |-------|    |-------|
    D            E            F    
|-------|    |-------|    |-------|
|       |    |       |    |       |
|   O   |    |   O---|    |   O   |
|   |   |    |       |    |   |   |
|-------|    |-------|    |-------|
    G            H            I    
(图 1)

现在需要用最少的移动,将9个时钟的指针都拨到12点的位置。共允许有9种不同的移动。如下表所示,每个移动会将若干个时钟的指针沿顺时针方向拨动90度。

移动      影响的时钟
1         ABDE
2         ABC
3         BCEF
4         ADG
5         BDEFH
6         CFI
7         DEGH
8         GHI
9         EFHI

输入:
9个整数,表示各时钟指针的起始位置,相邻两个整数之间用单个空格隔开。其中,0=12点、1=3点、2=6点、3=9点。

输出:
输出一个最短的移动序列,使得9个时钟的指针都指向12点。按照移动的序号从小到大输出结果。相邻两个整数之间用单个空格隔开。

输入样例:

3 3 0 
2 2 2 
2 1 2

样例输出:

4 5 8 9

解题思路:
暴力枚举

代码:

//解法一:递归 
#include <iostream>
#include <bitset>
#include <algorithm>
#include <functional>
#include <cstring>
using namespace std;

int oriClocks[9];
int clocks[9];
const char * moves[9] = { "ABDE","ABC","BCEF","ADG","BDEFH","CFI","DEGH","GHI","EFHI" };
int moveTimes[9] = {0};
int result[9];
int minTimes = 4*9;

void Enum(int n) 
{
	if( n >= 9 ) {
		memcpy(clocks,oriClocks,sizeof(clocks));
		int totalTimes = 0;
		for( int i = 0;i < 9 ; ++ i ) { 
			if( moveTimes[i] ) { 
				for( int k = 0; moves[i][k]; ++k) {
					clocks[moves[i][k]-'A'] = (clocks[moves[i][k]-'A'] + moveTimes[i]) % 4;//计算余数
					totalTimes += moveTimes[i];
				}
			}
		}

		int i;
		for( i = 0;i < 9; ++i )
			if( clocks[i])
				break;//存在时钟不是12点,移动失败
		if( i == 9) {
			if( minTimes > totalTimes) {
				minTimes = totalTimes;
				memcpy(result,moveTimes,sizeof(result)); 
			} 
		}

		return ;
	}

	for( int i = 0;i < 4; ++ i ) {
		moveTimes[n] = i;
		Enum(n+1);
	}
	return ;
} 

int main()
{
	for( int i = 0;i < 9 ; ++i )
		cin >> oriClocks[i];
	Enum(0);
	for( int i = 0; i < 9; ++i )//遍历每一种移动方式
		for( int k = 0; k < result[i] ; ++ k )//遍历移动次数
			cout << i+1 << " ";
	return 0;
}
//部分递归结果如下:
/*
moveTimes={0,0,0,0,0,0,0,0,0,}
moveTimes={0,0,0,0,0,0,0,0,1,}
moveTimes={0,0,0,0,0,0,0,0,2,}
moveTimes={0,0,0,0,0,0,0,0,3,}
只有8的可能性 
moveTimes={0,0,0,0,0,0,0,1,0,}
moveTimes={0,0,0,0,0,0,0,1,1,}
moveTimes={0,0,0,0,0,0,0,1,2,}
moveTimes={0,0,0,0,0,0,0,1,3,}

moveTimes={0,0,0,0,0,0,0,2,0,}
moveTimes={0,0,0,0,0,0,0,2,1,}
moveTimes={0,0,0,0,0,0,0,2,2,}
moveTimes={0,0,0,0,0,0,0,2,3,}

moveTimes={0,0,0,0,0,0,0,3,0,}
moveTimes={0,0,0,0,0,0,0,3,1,}
moveTimes={0,0,0,0,0,0,0,3,2,}
moveTimes={0,0,0,0,0,0,0,3,3,}
7+8的可能性 
*/

总结:

  1. 语法问题
    1)bitset创造一个内含位或布尔值且大小固定的数组(array)。当需要管理各种标识,并需要以标识的任意组合表现变量时,即可使用bitset模板类。
    2) 注意memcpy用法。
  2. 逻辑问题
    1)递归是从n=8开始,moveTimes[8]=0,1,2,3,进行只有移动方式8的列举; 然后返回上一级,moveTimes[7]=0,Enum(9)再次得到moveTimes[8]=1,进行移动方式7+8的组合列举,以此类推。
标签
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!