phpexcel遍历所有列_“枚举”真的要尝试所有情况吗?

枚举

算法思想

基于逐个尝试答案的一种问题求解策略

优化策略

枚举虽然是一种逐个尝试的问题求解策略,但是也是可以进行优化的,有些可能的答案或者可能的猜测在进行稍加判断后,就可以直接排除掉,从而避免代入问题进行求解,进一步节省时间。具体的优化方法,往往要根据具体问题具体分析,没有统一的标准。

基本例题

case 1 :完美立方

问题描述:形如​的等式被称为完美立方等式。例如:​。编写一个程序,对任意给定的整数(​,寻找所有的四元组​,使得​,其中​大于1,小于等于​,且​

  • 解题(优化)思路
    ​ 范围的确定,从而避免了将所有可能全部尝试的,提高算法效率。
示例代码
#include <iostream>
using namespace std;
int main() {
 int N;
 cin>>N;
 int a,b,c,d;
 for (a=2; a<=N; a++){
  for (b=2; b<=N; b++){
   for (c=b; c<=N; c++){
    for (d=c; d<=N; d++){
     if(a*a*a == b*b*b + c*c*c + d*d*d){
      cout<<"Cube = "<<a<<", Triple = ("<<b<<","<<c<<","<<d<<")"<<endl;
     }
     
    }
   }
  }
 } 
 return 0;
} 

case 2 :生理周期

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

  • 解题(优化)思路
    一般思路即是将所有日子进行判断,看是否是体力、情商、智商的三高峰,这种做法需要将所有的日子遍历一遍进行判断,时间成本太高。可以采用如下方法减少时间成本:从给定日子 ​ 开始,找到第一个体力高峰,然后再以体力高峰的时间间隔 ​ 递增,判断该天是否也是情商高峰,直到找到体力、情商的双高峰,之后以体力、情商高峰双高峰的时间间隔 ​ 递增,判断该天是否也为智商高峰,直到找到体力、情商、智商的三高峰,即为问题答案。该方法减少了对大量不可能日子的判断,从而有效的额提高了算法效率。
示例代码
#include <iostream>
using namespace std;
# define N 21252
int main() {
 int p,e,i,d,caseNo = 0;
 while(cin>> p >> e >> i >> d && p != -1) {
  ++ caseNo;
  int k = 0;
  for(k = d+1; (k-p)%23 != 0; ++k);
  cout<<k<<endl;
   for(; (k-e)%28 != 0; k+=23);
    for(; (k-i)%33 != 0; k+=23*28);
     cout<<"Case "<<caseNo<<": the next triple peak occurs in "<<k-d<<" days"<<endl;
 }
 return 0;
} 

case 3:称硬币

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

  • 解题(优化)思路
    对于每一枚硬币先假设它是轻的,看这样是否符合称量结果。如果符合,问题即解决。如果不符合,就假设它是重的,看是否符合称量结果。把所有的硬币都试一遍,一定能找到特殊硬币。
示例代码
#include <iostream>
#include <cstring> 
using namespace std;
char Left[3][7];  // 天平左边硬币
char Right[3][7];  // 天平右边硬币 
char result[3][7];  // 结果
bool IsFake(char c, bool light); // 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."<<endl;
    break;
   }
   else if(IsFake(c,false)) {
    cout<<c<<" is the counterfeit coin and it is heavy."<<endl;
   }
  }
 }
 return 0;
} 
​
// 判断是否为假币 
bool IsFake(char c, bool light) {
 // light 为真表示假设假币为轻,否则表示假设假币为重
 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; 
}

case 4:熄灯问题

问题描述:有一个由按钮组成的矩阵,其中每行有6个按钮,共5行。每个按钮的位置上有一盏灯。当按下一个按钮后,该按钮以及周围位置(上边、下边、左边、右边)的灯都会改变一次。即,如果灯原来是点亮的,就会被熄灭;如果灯原来是熄灭的,则会被点亮。在矩阵角上的按钮改变3盏灯的状态;在矩阵边上的按钮改变4盏灯的状态;其他的按钮改变5盏灯的状态。

68dd60b5216b4f45be272f97db8afd4e.png

在上图中,左边矩阵中用X标记的按钮表示被按下,右边的矩阵表示灯状态的改变。对矩阵中的每盏灯设置一个初始状态。请你按按钮,直至每一盏等都熄灭。与一盏灯毗邻的多个按钮被按下时,一个操作会抵消另一次操作的结果。在下图中,第2行第3、5列的按钮都被按下,因此第2行、第4列的灯的状态就不改变。

bd7e86e11f04bec36c75e64b2a2d6f1b.png

请你写一个程序,确定需要按下哪些按钮,恰好使得所有的灯都熄灭

  • 关键点
    • 第2次按下同一个按钮时,将抵消第1次按下时所产生的结果。因此,每个按钮最多只需要按下一次。
    • 各个按钮被按下的顺序对最终的结果没有影响。
    • 对第1行中每盏点亮的灯,按下第2行对应的按钮,就可以熄灭第1行的全部灯。如此重复下去,可以熄灭第1、2、3、4行的全部灯。同样,按下第1、2、3、4、5列的按钮,可以熄灭前5列的灯。
  • 解题(优化)思路
    • 一般思路:每一个灯有 ​ 种状态,开或者关,分别用 ​、​ 表示,共有 ​ 盏灯,因此共有 ​ 种情况,分别对每一种情况进行判断即可,有可能会超时,且效率低下。
    • 优化思路:如果存在某个局部,一旦这个局部的状态被确定,那么剩余其他部分的状态只能是确定的一种,或者不多的 ​ 种,那么就只需枚举这个局部的状态即可。
      • 经过观察,本题的局部就是第1行
      • 因为第1行的各开关状态确定的情况下,这些开关作用过后,将导致第1行某些灯是亮的,某些灯是灭的。
      • 要熄灭第1行某个亮着的灯(假设位于第 ​ 列),那么唯一的办法就是要按下第2行第 ​ 列的开关(因为第1行的开关已经用过了。而第3行及其以后的开关不会影响到第1行)
      • 为了使第1行的灯全部熄灭,第2行的合理开关状态就是唯一的。
示例代码
#include <iostream>
#include <memory> 
#include <cstring>
using namespace std;
char oriLights[5];  // 灯的原始状态 
char lights[5];   // 变化之后灯的状态 
char result[5];   // 结果
​
// 一个字符(char) 有 8 位(bit) 
// 取字符 c 的第 i 个比特 
int GetBit(char c, int i) {
 return (c>>i) & 1;   // 移位 + 与运算 
} 
​
// 设置字符 c 的第 i 个比特为 v 
void SetBit(char & c, int i, int v) {
 if (v) {
  c |= ( 1 << i );  // 移位 + 或运算 
 }
 else {
  c &= ~(1 << i);   // 移位 + 与预算 
 }
}
​
// 将字符 c 的第 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=1; 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);
   }
   cout<<"oriLights : "<<oriLights[i]<<endl;  // 存储灯的原始状态 
  }
  for (int n=0; n<64; n++) {  // n 从 1~63,一个值代表一种状态(每一行的) 
   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;
}
  • 代码细节分析
    • 考察到题目中所需的二维数组较小,且元素非0即1,采用 ​ 类型的一维数组进行存储,一个 ​ 类型有8个 bit,而题中仅仅用到了其中的6个,结合 位运算,可以降低时间复杂度和空间复杂度。
    • 采用二进制数枚举,如果有若干个比特(位),每个比特有 ​ 两种取值,这若干个比特所对应的所有组合都要枚举到,就可以采用一个整数来进行。原理如下:​个比特,可采用整数 ​ 进行枚举。​~​ 之间的每个整数对应一种组合,结合位运算实现。

总结与反思

虽然枚举是一种基于逐个尝试答案的一种问题求解策略,但是这并不意味着我们无法对其进行优化,有一些问题在进行稍加判断后就可以缩小尝试的范围,进而避免了一些不可能情况的尝试,可以极大的提高算法效率。

最后有需要工程文件的朋友可以在评论里说明(记得指明邮箱),小编看到后会第一时间发送到指定邮箱。文章如有不严谨之处,欢迎大家指正!!!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值