一、递推
首先了解递推和递归的区别
(递归):树结构,将一个问题分解成若干个相同的子问题
(递推):是通过子问题的解来得到原问题的解,按顺序依次求得解,一个一个枚举
例题:费解的开关
你玩过“拉灯”游戏吗?
25 盏灯排成一个 5x5 的方形。
每一个灯都有一个开关,游戏者可以改变它的状态。
每一步,游戏者可以改变某一个灯的状态。
游戏者改变一个灯的状态会产生连锁反应:和这个灯上下左右相邻的灯也要相应地改变其状态。
我们用数字 1 表示一盏开着的灯,用数字 0表示关着的灯。
下面这种状态
10111
01101
10111
10000
11011
在改变了最左上角的灯的状态后将变成:
01111
11101
10111
10000
11011
再改变它正中间的灯后状态将变成:
01111
11001
11001
10100
11011
给定一些游戏的初始状态,编写程序判断游戏者是否可能在 6 步以内使所有的灯都变亮。
输入格式
第一行输入正整数 n,代表数据中共有 n 个待解决的游戏初始状态。
以下若干行数据分为 n 组,每组数据有 5 行,每行 5 个字符。
每组数据描述了一个游戏的初始状态。
各组数据间用一个空行分隔。
输出格式
一共输出 n行数据,每行有一个小于等于 6 的整数,它表示对于输入数据中对应的游戏状态最少需要几步才能使所有灯变亮。
对于某一个游戏初始状态,若 6 步以内无法使所有灯变亮,则输出 −1。
数据范围
0<n≤500
输入样例:
3
00111
01011
10001
11010
11100
11101
11101
11110
11111
11111
01111
11111
11111
11111
11111
输出样例:
3
2
-1
解题思路:
1.下一行的操作完全取决与上一行,依次枚举到最后一行为止,最后一行的状态无法再继续改变
2.每一个5位的01字符串,可以看成是二进制的方式,要得到这个字符串每一位的状态,就>>i & 1,按位与,(1的二进制数是000001)
3.5位的二进制数,所有的情况共有2的5次方-1 = 31次,这31个数,每个都表示一种操作状态
(状态所决定)
4.操作一个格子四周的格子时,用偏移量表示,当前操作的格子坐标为(0,0),就可以表示出周围4个格子的坐标
代码实现:
相关技巧:
1.memcpy()进行内存的拷贝,可以是任意数据类型,以本题数组为例
memcpy(数组1,数组2,长度) 将数组2的内容复制给数组1,复制的长度为所给长度
2. 这里的op从1~31,因为是位运算,所以是1~31的二进制和1的二进制从右向左进行与的操作,比如10100(20)的op,意思是第2、4位(右到左01234)需要按,不是代表灯有没有亮,32种状态其实是哪些灯要按,一种op对应一种固定的按法,所以是&1==1或者^1==0。同理,调用方法这里应该是turn(0,4-i)
3.g[][]里面存的是字符‘1’和‘0’,但是0的ascll码是48,二进制是110000,和1异或后是110001,也就是1(异或:相同为0,不同为1)
4.第一行的操作1表示需要按,从第二行开始,0表示状态是灭的,下一行需要按
#include<bits/stdc++.h>
using namespace std;
const int N=6; //题目中的数据是5X5的二维字符数组,在结尾处有\0
//g[][]用来存储25盏灯的初始状态
char g[N][N],backup[N][N]; //backup[][]用来备份这25盏灯
int dx[5]={-1,0,1,0,0},dy[5]={0,1,0,-1,0}; //执行开关灯操作对应的五个位置
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; //字符1的ASCII码为49,0的ASCII码为48转化为二进制最后一位分别是1,0
} //^表示异或运算 0^1=1,1^1=0
} //通过位运算来优化操作
int main()
{
int q;
cin>>q;
while(q--) //执行q次操作
{
for(int i=0;i<5;i++) cin>>g[i]; //输入25盏灯的状态
int res = 10; //初始化答案,只要大于6即可
for(int op=0;op<32;op++) //第一行一共5个元素可以操作2^5=32次
{ //从一行开始枚举进行操作
memcpy(backup,g,sizeof g); //备份25盏灯
int step=0; //step用来存储操作的次数
for(int i=0;i<5;i++)
{
if(op>>i&1) //右移i位与1相当于表示第一行第5-i个位置
{
step++; //操作步数加1
turn(0,4-i); //进行操作
}
}
for(int i=0;i<4;i++)
{
for(int j=0;j<5;j++)
{
if(g[i][j]=='0') //如果这个位置的灯是灭的
{
step++; //步骤数加1
turn(i+1,j); //对该位置的下一行进行操作
}
}
}
bool dark=false; //dark变量表示最后一行是否全亮
for(int i=0;i<5;i++)
{
if(g[4][i]=='0') //如果最后一行出现不亮的灯0
{
dark=true; //dark为真
break; //终止循环
}
}
if(!dark) res=min(res,step); //最后一行全都亮表示所有灯都点亮了
memcpy(g,backup,sizeof g); //本次循环结束后将初始状态复原到g数组
}
if(res>6) res=-1; //大于6输出-1
cout<<res<<endl; //输出答案
}
return 0;
}
例题2:翻硬币
小明正在玩一个“翻硬币”的游戏。
桌上放着排成一排的若干硬币。我们用 * 表示正面,用 o 表示反面(是小写字母,不是零)。
比如,可能情形是:**oo***oooo
如果同时翻转左边的两个硬币,则变为:oooo***oooo
现在小明的问题是:如果已知了初始状态和要达到的目标状态,每次只能同时翻转相邻的两个硬币,那么对特定的局面,最少要翻动多少次呢?
我们约定:把翻动相邻的两个硬币叫做一步操作。
输入格式
两行等长的字符串,分别表示初始状态和要达到的目标状态。
输出格式
一个整数,表示最小操作步数
数据范围
输入字符串的长度均不超过100。
数据保证答案一定有解。
输入样例1:
**********
o****o****
输出样例1:
5
输入样例2:
*o**o***o***
*o***o**o***
输出样例2:
1
代码实现:
看似是找到一个最优解,但是实际上若有解,则只有一个解且为最优解
#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
const int N = 110;
char start[N],fin[N];//开始状态和结束状态
void turn(int i)
{
if(start[i]=='*')start[i]='o';
else start[i]='*';
return;
}
int main(){
cin>>start>>fin;
int step = 0;
int n = strlen(start);
for(int i=0;i<n;i++)
{
if(start[i]!=fin[i])
{
turn(i),turn(i+1);
step++;
}
}
cout<<step<<endl;
return 0;
}