对每一道题,首先分析问题抽象考察什么模型,分析数据范围,选用最佳的解决方案;考试时候一定要考虑清楚再写
最简单的递归-斐波那契数列
自己调用自己
92.递归实现指数型枚举
红圈:当我们从当前节点向下执行到最后一层向上回溯时需要"恢复现场"以保证当前节点的状态不受其他分支代码的影响
恢复现场是递归中十分重要的一个环节
#include<bits/stdc++.h>
using namespace std;
const int N=16;//数据范围 一般用const修饰
int n;
int st[N];//st数组记录当前每个位置的状态:0表示未处理,1表示选,2表示不选
void dfs(int u)//u表示当前在做第几位的排列方案-分支节点位置
{
if(u>n)
{
for(int i=1;i<=n;i++)
{
if(st[i]==1)
{
printf("%d ",i);
}
}
printf("\n");//输出一种排列方案后换行
return;
}
st[u]=2;
dfs(u+1); //第一个分支:不选
st[u]=0; //恢复现场
st[u]=1;
dfs(u+1); //第二个分支:选
st[u]=0;
}
int main()
{
cin>>n;
dfs(1);
return 0;
}
94.递归实现排列型枚举
放缩法证明时间复杂度
#include<bits/stdc++.h>
using namespace std;
const int N=10;
int n;//变量定义为全局变量,初始值为0;局部变量值随机
int state[N];//0表示还未放数,1~n表示放了那个数
bool used[N];//true 表示用过,false 表示还未用过
void dfs(int u)
{
if(u>n)
{
for(int i=1;i<=n;i++) printf("%d ",state[i]);//打印方案
puts("");
return;
}
//依次枚举每个分支,即当前位置可以填那些数
for(int i=1;i<=n;i++)
{
if(!used[i])
{
state[u]=i;
used[i]=true;
dfs(u+1);
state[u]=0;
used[i]=false;//恢复现场
}
}
}
int main()
{
cin>>n;
dfs(1);
return 0;
}
枚举
1.排列考虑顺序,比如对1.2.3三个数字排列:123.132.213.231.312.321
2.组合不考虑顺序,比如从1~5任意选择三个数字:123.124.125… …组合的数不能重样,比如不能存在123.231这样重复数字组合。
我们采用升序组合,保证每次新加的数比前面的一个数大,123.124.125.234.234…
也就是a1<a2,a2<a3…a(n-1)<a(n)
为了防止调试时数据越界,我们在const int N=;一般多开一些数据范围
剪枝:以上面的组合搜索树为例,如果某一分支节点下没有满足条件的解,我们将该分支剪掉-提前return 以提高算法效率
以本题为例,当我们枚举到第u个位置的数,此时已经枚举完成u-1个数,剩下(m-start+1)个数,若 u-1+n-start+1<m 则说明如果把后面的数全选上都不够m个,说明当前分支一定无解->提前退出,提高算法效率
#include<bits/stdc++.h>
using namespace std;
const int N=30;
int n,m;
int way[N];
void dfs(int u,int start)//u表示当前枚举到那个位置;start表示从那个数开始枚举
{
if(u + n - start < m) return;//剪枝 如果把后面的数全选上都不够m个,说明当前分支一定无解->提前退出,提高算法效率
if(u == m+1)//当u==m+1 说明枚举完成最后一个数 输出一种组合
{
for(int i=1;i<=m;i++) printf("%d ",way[i]);
puts("");//换行
return;
}//边界
for(int i = start ; i <= n ; i ++)
{
way[u] =i;
dfs( u + 1 ,i + 1);
way[u] = 0;//虽然后面的值会把该位置覆盖掉,为了更严谨依然置0 恢复现场
}
}
int main()
{
scanf("%d %d",&n,&m);//n个整数当中选出m个数组合
dfs(1,1);//从第一个位置开始枚举,第一个枚举的数为1
return 0;
}
1209.带分数
关于空间复杂度:1Byte(字节)=8bit(比特)1bit就是8二进制位;以空间限制64MB为例, 64 MB = 642^20 Byte,可以开1.610^7个int;当前们在代码中开一个数组,在该数组尚未被使用的情况下,这个数组不占任何空间
1.正整数 n = a + b / c ,整数a b c由数字1~9组成且不重复 =>满足等式 b = n * c - a * c 检查a,c是否满足组合要求
2.枚举a,a每个叶子节点枚举c,由 b = n * c - a * c 的出b并判断等式是否成立
#include<bits/stdc++.h>
using namespace std;
const int N = 20;
int n; //表示成带分数形式的正整数
bool st[N],backup[N];//记录当前位置是否已经被数字占用 true占用 false 未被使用
int ans;//记录一共有多少种组合方案
bool check(int a,int c)
{
int b = n * c - a * c;
if(!a || !b || !c) return false;
memcpy(backup, st, sizeof st);
while(b)//当 b > 0,拆解b判断组成b的数字当中是否有0
{
int x = b % 10;//取个位
b /= 10; //删个位
if(!x || backup[x]) return false; // x不能为0 backup[x]判断x之前是否出现过
backup[x] = true; //标记x已经被使用过
}
for(int i = 1; i <= 9; i ++ )
{
if(!backup[i])
{
return false;
}
}
return true;
}
void dfs_c(int u, int a, int c)
{
if(u == n) return;//当前n个数字全部用完
if(check(a,c)) ans ++;//判断a、c是否满足要求
for(int i = 1 ;i <= 9; i ++ )
{
if(!st[i])
{
st[i] = true;
dfs_c(u + 1, a, c * 10 + i);
st[i] = false;
}
}
}
void dfs_a(int u,int a)
{
if (a >= n) return; // 得到的a比带分数n大
if(a > 0) dfs_c(u, a, 0);
for(int i=1; i <= 9; i ++ )
{
if(!st[i])
{
st[i] = true;
dfs_a(u + 1, a * 10 + i); //枚举完u个数字后在a后面插入数字i,把a作为一个分支节点
st[i] = false;//恢复现场
}
}
}
int main()
{
cin>>n;
dfs_a(0, 0);//(当前已经枚举/用完多少个数字,a从0开始 枚举生成正整数)
cout<<ans<<endl;
return 0;
}
递推
**#include<bits/stdc++.h>
using namespace std;
//滚动数组
int main()
{
int n;
cin>>n;
int a=0,b=1;
for(int i=1;i<=n;i++)
{
cout<<a<<' ';
int fn=a+b;
a=b,b=fn;
}
return 0;
}**
#include<iostream>
#include<cstring>
#include<algorithm>
using namespace std;
const int N=50;
int fib[N];
int main()
{//基础写法
int n;
cin>>n;
fib[1]=0,fib[2]=1;
for(int i=1;i<=n;i++)
{
if(i>=3) fib[i]=fib[i-1]+fib[i-2];
cout<<fib[i]<<" ";
}
return 0;
}
1208.翻硬币
递归题-费解开关的一个简化版
类型题:179.八数码
模型分析—
1.2.思路:这里我们把一步操作后的状态一个点,每一步不同操作形成边连接不同状态的点,从初始状态到目标状态就是求两个点间的最短距离 ;把每一步发明转看作一个开关,在每个开关最多按1次情况下每个问题最多有1种方案/无解
数据范围:字符串长度最大为100,相邻硬币翻动一次,每次操作最多有99种选择,最多翻动50次达到目标状态(极端情况下,100枚硬币全部朝正面/反面=》<=50^99
代码逻辑–
1.从第一个灯泡依次比较当前灯泡与目标灯泡状态,如不一样则做一次“开关”,一样则跳过继续比较,最终统计一共按开关的次数–枚举
#include<bits/stdc++.h>
using namespace std;
const int N = 110; //字符串长度最大为100(枚硬币),防止越界问题多开一部分数据
int n; //字符串长度
char start[N],aim[N];//存放初始状态与目标状态 数组
void turn(int i) //start[]为全局声明,所以不用传入start参数
{
if(start[i] == '*') start[i] ='o';
else start[i] = '*';
}
int main()
{
cin>>start>>aim;
n = strlen(start);
int res = 0;
for(int i=0;i<n-1;i++)
{
if(start[i] != aim[i]) // 比较两个位置上的状态是否一致,不一致则翻转一次
{
turn(i),turn(i+1);
res++;
}
}
cout<<res<<endl;
return 0;
}
116.飞行员兄弟(!看不懂!)
208.开关问题
数据范围:共2^16种方案,每种方案有16个开关,每个开关操作7个开关
每次操作完判断16个开关的状态并记录,最多全亮/灭要记录16次
2^16*(16*7+16+16)
#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
#include<vector>
using namespace std;
#define x first
#define y second
typedef pair<int ,int> PII; //用pair存放二维数组 -- 简化代码
const int N = 5;
char g[N][N],backup[N][N];
// 映射 -- 返回第 x 行 y 列位置上的数
int get(int x,int y)
{
return x * 4 + y;
}
// 改变灯泡状态
void turn_one(int x, int y)
{
if(g[x][y] == '+') g[x][y] = '-';
else g[x][y] = '+';
}
void turn_all(int x, int y)
{
//将在 x行 和 y列 位置上的灯泡状态改变 <--按(x,y)一次开关
for(int i = 0; i < 4; i++)
{
turn_one(x,i);
turn_one(i,y);
}
turn_one(x,y); //上述(x , y)位置开关被turn两次,这里恢复到1次
}
int main()
{
for(int i = 0; i < 4; i++)
{
for(int j = 0; j < 4; j ++ )
{
cin >> g[i][j];
}
}
vector<PII> res; //记录方案所需要的结构
//枚举出所有方案共2^16种
for(int op = 0; op < 1 << 16; op ++ ) //1左移16位--2^16
{
for(int i = 0; i < 4; i ++ )
{
vector<PII> temp; //用来存放当前位置上的方案
//由于我们要对所有灯泡状态进行操作,这里先把状态备份一下--最后要进行还原
memcpy(backup,g,sizeof g);
// 进行操作
for(int i = 0; i < 4; i ++)
{
for(int j = 0; j < 4; j ++)
{
if(op>>get(i,j) & 1) //如果当前位置是1,get作用就是返回二进制位中那一位是第几位-从而判断是否为1
{
temp.push_back({i,j});
//按一下开关
turn_all(i,j);
}
}
}
//判断所有灯泡是否全亮
bool has_closed = false;
for(int i = 0; i < 4; i ++ )
{
for(int j = 0; j < 4; j ++)
{
if(g[i][j] == '+')
{
has_closed = true;
}
}
}
if( has_closed == false)
{
// 记录下方案
if(res.empty() || res.size() > temp.size() ) res = temp;
//若当前方案为空或记录到的步数严格>当前方案的操作步数
}
memcpy(g,backup,sizeof g); //还原
}
}
cout<<res.size() << endl;
for(auto op : res) cout << op.x + 1 << " " << op.y + 1 << endl;
return 0;
}