文章目录
B.递推
基础知识
1.什么是递推?
所谓递推,是指从已知的初始条件出发,依据某种递推关系,逐次推出所要求的各中间结果及最后结果。其中初始条件或是问题本身已经给定,或是通过对问题的分析与化简后确定。从已知条件出发逐步推到问题结果,此种方法叫顺推。从问题出发逐步推到已知条件,此种方法叫逆推。无论顺推还是逆推,其关键是要找到递推式。这种处理问题的方法能使复杂运算化为若干步重复的简单运算。
2.递推与递归
递归:把某一个问题分成若干个同类子问题,即先求原问题,然后递归调用子问题(自顶而下)
递推:反过来,先求子问题的结果,再用子问题直接把原问题计算出来(自下而上)
3.模型:
当后一项受前一项的影响时:考虑用递推
递推式不一定是具体的运算式,也可能是抽象的逻辑顺序
可用递推算法求解的题目一般有以下两个特点:
1、问题可以划分成多个状态;
2、除初始状态外,其它各个状态都可以用固定的递推关系式来表示。
在我们实际解题中,题目不会直接给出递推关系式,而是需要通过分析各种状态,找出递推关系式。
例题
一、简单斐波那契
1.解题思路:
要注意边界:f(1)=0,f(2)=1。
递推式:n>=3,f(n)=f(n-1)+f(n-2),即每一项只于前两项有关系
2.代码:
//数组做法
#include <iostream>
using namespace std;
int main()
{
int n;
cin>>n;
int f[50];
f[1]=0,f[2]=1; //边界:初始值
for(int i=3;i<=n;i++) f[i]=f[i-1]+f[i-2]; //递推式
for(int i=1;i<=n;i++) cout<<f[i]<<" ";
return 0;
}
//优化:因为每一项只与前两项有关,所以可以用变量来存
#include <cstring>
#include <iostream>
#include <algorithm>
using namespace std;
int main()
{
int a = 0, b = 1;
int n;
cin >> n;
for (int i = 0; i < n; i ++ )
{
cout << a << ' ';
int c = a + b;
a = b, b = c;
}
cout << endl;
return 0;
}
二、费解的开关
1.解题思路:
(1)题意:变一个格子会把上下左右四个格子都给翻转一次
(2)分析问题,发现性质:
①按得顺序可以任意:按法和顺序没有关系,只和周围的十字形格子按得次数有关
②每个格子最多只能按一次:按两次相当于没按,还多了两步
(3)步骤:
①枚举第一行操作:
第一行所有状态通过枚举来选,每一个开关要么按要么不按,所以会有2的n次方种不同的按法。(一共n个数,每个数两种方案)
②操作后面几行:
假设第一行已经按完了,按完之后就不能再按第一行其他格子了,现在操作第二行
可以发现:此时,对于第一行暗着的格子,由于第一行是固定了的,所以只能通过下一个格子来改变天他,即第二行的格子来操作,所以第二行的按法是唯一确定的(如果上一个格子是暗的,那么这一个格子必须改变;如果上一个格子是亮的,那么这个格子必须不能变。)
同理:对于第三行每个格子的操作完全取决的第二行,前两行不能再操作了,第一行决定了第二行,第二行决定了第三行。
结论:每一行开关的操作,完全被前一行的灯的亮灭状态所决定。(递推关系)
第一行操作改变了第一行的状态,第一行的状态决定第二行的操作,第二行操作完之后改变了第二行的状态,第二行的状态决定了第三行的操作,以此类推,直到最后一行,最后一行的操作会被前一行的状态确定,此时整个所有开关全部被确定。只能保证前n-1行全部亮了,最后一行特判一下,最后一行如果有灭的那就无解了,如果全亮更新一下最下步数。
(4)细节问题:
①如何枚举第一行操作?
可以用递归实现指数类型枚举,也可以用循环+位运算操作,用一个二进制01串,用0~31枚举一遍即可
②如何写变化的函数turn(x,y)?
存一下偏移量,然后直接加上偏移量即可
③时间复杂度?
大约二百万左右
2.代码:
#include<iostream>
#include<cstdio>
#include<algorithm>
#include<cstring>
using namespace std;
const int N = 6;
int dx[N] = {-1, 0, 1, 0, 0}, dy[N] = {0, 1, 0, -1, 0}; //定义偏移量,四个方向
char g[N][N], backup[N][N];
// 这个操作是把(x, y)以及上下左右的灯都变成相反的颜色
void turn (int x, int y)
{
for (int i = 0; i < 5; i ++ )
{
int a = x + dx[i], b = y + dy[i];
//如果在边界外边,直接忽略即可
if (a < 0 || a >= 5 || b < 0 || b >= 5) continue;
g[a][b] ^= 1; //小技巧:异或,不同的时候就变成相反的数
}
}
int main()
{
int n;
scanf("%d", &n);
while(n -- )
{
// 按行输入,把每一行当成一个字符串
for (int i = 0; i < 5; i ++ ) cin >> g[i];
int res = 10;
// 这里我们枚举了第一行的32种按法,不用管是亮是灭,把第一行所有情况都按一遍
// 按每种情况的第一行,去遍历接下来的行
// 枚举32种第一行的按法只是可能会减少步数,如果直接从第二行开始答案一定是固定的了,找不到最优解或者可能没有解
for (int op = 0; op < 32; op ++ )
{
// 我在对这种情况操作的时候,得先备用一下
// 把原始数组备份一下,然后操作g,操作完了还原,然后再操作
memcpy(backup, g, sizeof g);
int step = 0;
// 第一行的按法(在这里 1 表示按了, 0 表示不按),这里只是为了输出第一行按完之后的状态
for (int i = 0; i < 5; i ++ )
if (op >> i & 1) // 数字2 对应了 00010 表示第2个位置的按一下
// 00010 >> 1 & 1 是1 所以turn(0, 1) 就是第一行第二个位置
{ // 数字3 对应了00011 表示第1 和第2个位置的按一下
step ++ ;
turn (0, i);;
}
// 然后通过第一行按完之后的状态,按234行
for (int i =0; i < 4; i ++ )
for (int j = 0; j < 5;j ++ )
if (g[i][j] == '0')
{
step ++;
turn (i + 1, j); // 如果这个位置是灭的,就按下一行对应的位置
}
//特判最后一行
bool dark = false;
for (int j = 0; j < 5; j ++ )
if (g[4][j] == '0')
{
dark = true;
break;
}
// 对于32种情况的这一种,如果所有的全亮就记录下步数(事实上只记录了最后一行是否dark)
if (!dark) res = min(res, step);
memcpy (g, backup, sizeof g);
}
if(res > 6) res = -1;
cout << res << endl;
}
return 0;
}
参考题解:
AcWing 95. 单纯解释一下很多高赞题解没有说清楚的东西 - AcWing
三、飞行员兄弟
1.解题思路:
不要生搬硬套,看似和费解的开关类似,但是他的数据范围很小,所以直接暴力枚举所有方案,不像费解的开关只是枚举第一行的所有方案。
所有开关问题的共同点:
①每个最多按一次
②不用考虑顺序
做法:
①枚举所有方案
②按照该方案对所有灯泡进行操作
③判断,记录所有方案
2.代码:
#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
#define x first
#define y second
typedef pair<int, int> PII;
char mp[4][4];
PII path[20]; //存放结果路径
bool finish;
void turn_one(int x, int y)
{
if(mp[x][y] == '+') mp[x][y] = '-';
else mp[x][y] = '+';
}
void turn(int x, int y)
{
turn_one(x, y);
for(int i = 0; i < 4; i ++)
{
turn_one(x, i);
turn_one(i, y);
}
}
bool judge()
{
for(int i = 0; i < 4; i ++)
for(int j = 0; j < 4; j ++)
{
if(mp[i][j] == '+') return false;
}
return true;
}
void dfs(int x, int y, int cnt) //当前遍历到(x, y)处, 且要进行第cnt次操作
{
if(finish == true) return; //剪枝
if(x == 4)
{
if(judge() == true)
{
cout << cnt << endl;
for(int i = 0; i < cnt; i ++) cout << path[i].x << " " << path[i].y << endl;
finish = true;
}
return;
}
if(y == 4)
{
dfs(x + 1, 0, cnt);
return;
}
//按开关
path[cnt].x = x + 1;
path[cnt].y = y + 1;
turn(x, y);
dfs(x, y + 1, cnt + 1);
turn(x, y);
path[cnt].x = 0;
path[cnt].y = 0;
//不操作
dfs(x, y + 1, cnt);
}
int main()
{
for(int i = 0; i < 4; i ++) cin >> mp[i];
dfs(0, 0, 0);
}
四、翻硬币
1.解题思路:
解法一:宽搜BFS(可能超时)
一般什么情况下考虑宽搜:给定一些操作
解法二:递推:相当于一维费解的开关
2.代码:
#include <iostream>
#include <cstring>
#include <string>
using namespace std;
string s1,s2;
void turn(int i)
{
if(s1[i]=='*') s1[i]='o';
else s1[i]='*';
}
int main()
{
cin>>s1>>s2;
int cnt=0;
for(int i=0;i<s1.size();i++)
{
if(s1[i]!=s2[i])
{
cnt++;
turn(i);
turn(i+1);
}
}
cout<<cnt;
return 0;
}