搜索算法
搜索是一种有目的地枚举问题的解空间中部分或全部情况,进而找到解的方法。然后,与枚举策略相比,搜索通常是有目的的查找,发现解空间的某一子集内不存在解时,它便会放弃对该子集的搜索,而不像枚举那般逐个地检查子集内的解是否为问题的解。
1.宽度优先搜索
宽度优先搜索策略从搜索的起点开始,不断地优先访问当前结点的邻居。也就是说,首先访问起点,然后依次访问起点尚未访问的邻居结点,再按照访问起点邻居的先后顺序依次访问它们的邻居,直到找到解或搜遍整个解空间。
POJ Catch That Cow
Description
Farmer John has been informed of the location of a fugitive cow and wants to catch her immediately. He starts at a point N (0 ≤ N ≤ 100,000) on a number line and the cow is at a point K (0 ≤ K ≤ 100,000) on the same number line. Farmer John has two modes of transportation: walking and teleporting.
* Walking: FJ can move from any point X to the points X - 1 or X + 1 in a single minute
* Teleporting: FJ can move from any point X to the point 2 × X in a single minute.
If the cow, unaware of its pursuit, does not move at all, how long does it take for Farmer John to retrieve it?
Input
Line 1: Two space-separated integers: N and K
Output
Line 1: The least amount of time, in minutes, it takes for Farmer John to catch the fugitive cow.
Sample Input
5 17
Sample Output
4
Hint
The fastest way for Farmer John to reach the fugitive cow is to move along the following path: 5-10-9-18-17, which takes 4 minutes.
题目大意
分析
代码
#include<iostream>
#include<cstdio>
#include<cstring>
#include<queue>
using namespace std;
const int MAXN = 100001;//N和K的范围都是0到100000
struct Status{
int n,t;//定义当前状态两个参数,位置n,和时间t
Status(int n,int t):n(n), t(t){}//结构体构造
};
bool visit[MAXN];
int BFS(int n,int k){
queue<Status> myQueue;
myQueue.push(Status(n, 0 ));//压入初始状态
visit[n] = true;//起始点已被访问
while(!myQueue.empty()){
Status current = myQueue.front();//取出队列的头作为当前的状态
myQueue.pop();//出队操作,将队列头删除,即删除当前的状态
if(current.n == k){
return current.t;//如果当前状态的n到达了搜索的最终条件,即返回时间t(题目所求的最短时间)
}
for (int i = 0; i < 3; ++i) {//题目中涉及到了3种状态转移函数
Status next(current.n, current.t+1);//初始化下一个状态值(利用结构体构造函数,先初始化为当前的位置,但是时间+1)
if(i == 0){
next.n +=1 ;
}else if(i == 1){
next.n -= 1;
}else{
next.n *=2;
}
if(next.n < 0 || next.n >= MAXN || visit[next.n]){
continue;//判断新状态是否合法以及是否以及被遍历过
}
myQueue.push(next);//进队操作,将新状态压入队列中
visit[next.n] = true;// 将此状态的位置设置为被遍历过
}
}
}
int main(){
int n, k;
scanf("%d%d",&n,&k);
memset(visit,false,sizeof(visit));
printf("%d\n",BFS(n,k));
}
POJ Find The Multiple
Description
Given a positive integer n, write a program to find out a nonzero multiple m of n whose decimal representation contains only the digits 0 and 1. You may assume that n is not greater than 200 and there is a corresponding m containing no more than 100 decimal digits.
Input
The input file may contain multiple test cases. Each line contains a value of n (1 <= n <= 200). A line containing a zero terminates the input.
Output
For each value of n in the input print a line containing the corresponding value of m. The decimal representation of m must not contain more than 100 digits. If there are multiple solutions for a given value of n, any one of them is acceptable.
Sample Input
2
6
19
0
Sample Output
10
100100100100100100
111111111111111111
题目大意
分析
代码
#include<iostream>
#include<cstdio>
#include<queue>
using namespace std;
void BFS(int n){
queue<long long> myQueue;
myQueue.push(1);//初始状态为1,压入到队列的头
while(!myQueue.empty()){
long long current = myQueue.front();
myQueue.pop();
if(current % n == 0){
printf("%lld\n",current);
return;
}
myQueue.push(current*10);
myQueue.push(current*10 + 1);
}
}
int main(){
int n;
while(scanf("%d", &n) != EOF){
if(n == 0){
break;
}
BFS(n);
}
return 0;
}
KY12 玛雅人的密码
题目描述
玛雅人有一种密码,如果字符串中出现连续的2012四个数字就能解开密码。给一个长度为N的字符串,(2=<N<=13)该字符串中只含有0,1,2三种数字,问这个字符串要移位几次才能解开密码,每次只能移动相邻的两个数字。例如02120经过一次移位,可以得到20120,01220,02210,02102,其中20120符合要求,因此输出为1.如果无论移位多少次都解不开密码,输出-1。
输入描述:
输入包含多组测试数据,每组测试数据由两行组成。
第一行为一个整数N,代表字符串的长度(2<=N<=13)。
第二行为一个仅由0、1、2组成的,长度为N的字符串。
输出描述:
对于每组测试数据,若可以解出密码,输出最少的移位次数;否则输出-1。
输入
5
02120
输出
1
代码
#include<iostream>
#include<queue>
#include<string>
using namespace std;
struct mitery{
int index;//移位次数
string s;//密码字符串
mitery(int i,string ss):index(i),s(ss){}
};
void BFS(string s){
queue<mitery> myQueue;
myQueue.push(mitery(0,s));//将初始状态压入队列头
while(!myQueue.empty()){
mitery current=myQueue.front();
myQueue.pop();
string currentString=current.s;
if(currentString.find("2012")!=string::npos){
cout<<current.index<<endl;//如果当前字符串以及可以找到2012
return ;
}
for(int i=0;i<currentString.size()-1;i++){//状态转移的方式:每次只能移动相邻的两个数字
swap(currentString[i],currentString[i+1]);
myQueue.push(mitery(current.index+1,currentString));
swap(currentString[i],currentString[i+1]);
}
}
cout<<-1<<endl;
}
int main(){
int n;
string s;
while(cin>>n>>s){
BFS(s);
}
return 0;
}
2.深度优先搜索
搜索过程中,通过放弃对某些不可能产生结果的子集的搜索,达到提高效率的目的。这样的技术被称为剪枝。
POJ A Knight's Journey
Description
Background
The knight is getting bored of seeing the same black and white squares again and again and has decided to make a journey
around the world. Whenever a knight moves, it is two squares in one direction and one square perpendicular to this. The world of a knight is the chessboard he is living on. Our knight lives on a chessboard that has a smaller area than a regular 8 * 8 board, but it is still rectangular. Can you help this adventurous knight to make travel plans?
Problem
Find a path such that the knight visits every square once. The knight can start and end on any square of the board.
Input
The input begins with a positive integer n in the first line. The following lines contain n test cases. Each test case consists of a single line with two positive integers p and q, such that 1 <= p * q <= 26. This represents a p * q chessboard, where p describes how many different square numbers 1, . . . , p exist, q describes how many different square letters exist. These are the first q letters of the Latin alphabet: A, . . .
Output
The output for every scenario begins with a line containing "Scenario #i:", where i is the number of the scenario starting at 1. Then print a single line containing the lexicographically first path that visits all squares of the chessboard with knight moves followed by an empty line. The path should be given on a single line by concatenating the names of the visited squares. Each square name consists of a capital letter followed by a number.
If no such path exist, you should output impossible on a single line.
Sample Input
3
1 1
2 3
4 3
Sample Output
Scenario #1:
A1
Scenario #2:
impossible
Scenario #3:
A1B3C1A2B4C2A3B1C3A4B2C4
代码
#include<iostream>
#include<cstdio>
#include<string>
#include<cstring>
using namespace std;
const int MAXN = 30;//列数和行数均不超过8(即8×8)的棋盘
int p,q;
bool visit[MAXN][MAXN];
int direction[8][2]{//只能走日字
{-1,-2},{1,-2},{-2,-1},{2,-1},{-2,1},{2,-1},{-1,2},{1,2}
};
bool DFS(int x,int y,int step,string ans){
if(step == p*q){//搜索成功
cout<<ans<<endl<<endl;
return true;
}else{
for (int i = 0; i < 8; ++i) {//遍历邻居节点
int nx = x + direction[i][0];//计算下一步的状态(x,y)——>(nx,ny)
int ny = y + direction[i][1];
char col = ny + 'A';
char row = nx + '1';//用A-Z来表示列,1-99来表示横行
if(nx<0||nx>=p||ny<0||ny>=q||visit[nx][ny]){
continue;//该点不合法或者已经被遍历过
}
visit[nx][ny]= true;//标记该点
if(DFS(nx,ny,step+1,ans+col+row)){
return true;//递归遍历
}
visit[nx][ny]=false;//取消该点
}
}
return false;
}
int main(){
int n;
scanf("%d",&n);//第一行中有一个正整数n,代表数据有n组。
int caseNumber = 0;
while (n--){
scanf("%d%d",&p,&q);//对于每组数据,都含有两个正整数p和q(1 <= p * q <= 26),代表棋盘有p行q列。
memset(visit,false,sizeof(visit));
cout<<"Scenrio #"<<++caseNumber<<":"<<endl;
visit[0][0]=true;//题目要求在所有可行的路径中输出字母表排序最小的那个解,这个解必定经过A1
if(!DFS(0,0,1,"A1")){//初始状态的坐标是A1(0,0,1,“A1”)
cout<<"impossible"<<endl<<endl;
}
}
}
POJ Square
Description
Given a set of sticks of various lengths, is it possible to join them end-to-end to form a square?
Input
The first line of input contains N, the number of test cases. Each test case begins with an integer 4 <= M <= 20, the number of sticks. M integers follow; each gives the length of a stick - an integer between 1 and 10,000.
Output
For each case, output a line containing "yes" if is is possible to form a square; otherwise output "no".
Sample Input
3
4 1 1 1 1
5 10 20 30 40 50
8 1 7 2 6 4 4 3 5
Sample Output
yes
no
yes
代码
/**
* 本题剪枝如下:
* (1)如果总长不能被4整除,则一定不可以构成正方形
* (2)如果某根木棍的长度大于边长side,那么必定无法构成正方形
* (3)如果当前木棍无法构成边,之后可以跳过相同的木棍(需要排序)
*/
#include<iostream>
#include<algorithm>
#include<cstdio>
#include<cstring>
using namespace std;
const int MAXN = 25;//棍子的个数是4到20
int side;//边长
int m;//木棍数量
int sticks[MAXN];//木棍长度
bool visit[MAXN];
bool DFS(int sum,int number,int position){//sum是当前拼凑的木棍长度,number是当前已经拼凑成的边长数目,position是当前木棍的编号
if(number==3){//如果总长能够整除4,且已经拼凑出3条边,那么剩下的木棍一定可以构成最后一条边。
return true;
}
int sample=0;//剪枝(3)
for (int i = position; i < m; ++i) {
if (visit[i]||sum+sticks[i]>side||sticks[i]==sample){
continue;//木棍被标记过、超过了边长、木棍之前被放弃过
}
visit[i]=true;//标记该木棍
if(sum+sticks[i]==side){//恰好形成边
if(DFS(0,number+1,0)){
return true;
}else{
sample=sticks[i];//记录木棍失败长度
}
}else{//没有形成边继续拼凑
if(DFS(sum+sticks[i],number,i+1)){
return true;
}else{
sample=sticks[i];//记录木棍失败长度
}
}
visit[i] = false;
}
return false;
}
bool Compare(int x,int y){
return x>y;
}
int main(){
int n;
scanf("%d",&n);//第一行中有一个正整数n,代表数据有n组。
while(n--){
int length = 0;//总长
scanf("%d",&m);//木棍数目
for (int i = 0; i < m; ++i) {
scanf("%d",&sticks[i]);//木棍长度
length += sticks[i];
}
memset(visit,false,sizeof(visit));
if(length%4!=0){//剪枝(1)
printf("no\n");
continue;
}
side = length / 4;//边长
sort(sticks,sticks+m,Compare);//从大到小排序
if(sticks[0]>side){//剪枝(2)
printf("no\n");
continue;
}
if(DFS(0,0,0)){//初始状态,第一根木棍开始
printf("yes\n");
}else{
printf("no\n");
}
}
return 0;
}
KY59 神奇的口袋
题目描述
有一个神奇的口袋,总的容积是40,用这个口袋可以变出一些物品,这些物品的总体积必须是40。John现在有n个想要得到的物品,每个物品的体积分别是a1,a2……an。John可以从这些物品中选择一些,如果选出的物体的总体积是40,那么利用这个神奇的口袋,John就可以得到这些物品。现在的问题是,John有多少种不同的选择物品的方式。
输入描述:
输入的第一行是正整数n (1 <= n <= 20),表示不同的物品的数目。接下来的n行,每行有一个1到40之间的正整数,分别给出a1,a2……an的值。
输出描述:
输出不同的选择物品的方式的数目。
输入
3
20
20
20
输出
3
代码
法一:递归
/**
* 法一 :递归 把物品数目n和物品体积数组a[100]设为全局变量;
* count(i,sum)表示从数组的第i个数开始往后统计的组合数和为sum的种类数,
* sum为组合数的和,则:cout(i,sum)=cout(i+1,sum-a[i])+cout(i+1,sum),
* 其中cout(i+1,sum-a[i])表示包含了a[i],即为从第i+1个数开始往后统计
* 组合数的和为sum-a[i]的种类数, 而cout(i+1,sum)表示不包含a[i], 即为从第i+1个数开始往后统计组合数的和为sum的种类数
*/
#include <iostream>
using namespace std;
int a[100];
int n=1;
int count(int i,int sum)
{
if(sum==0){return 1;} //找到一组和为sum的组合数;
if(i==n||sum<0) return 0;//i==n说明没有其他的数来组合,sum<0说明组合不出;
return count(i+1,sum-a[i])+count(i+1,sum);//从数组的第i为开始,包含a[i],和不包含;
}
int main()
{
while(cin>>n){
for(int i=0;i<n;i++)
cin>>a[i];
cout<<count(0,40)<<endl;
}
return 0;
}
法二:DFS
#include<iostream>
#include<algorithm>
#include<cstring>
using namespace std;
const int MAXN = 25;
const int total = 40; // 总重量
bool visit[MAXN]; // 标记数组
int matter[MAXN]; // 存放物品
int kind = 0; // 记录一共有多少种
int n; // 物品的数量
void DFS(int sum, int position) { // sum为当前已经凑的质量
if (sum == total) {
kind++; // 种数增加
return;
}
// 从第一件开始凑数
for (int i = position; i < n; i++) {
if (visit[i] || sum + matter[i] > total) {
continue;
}
visit[i] = true;
DFS(sum + matter[i], i);
visit[i] = false; // 回溯
}
}
int main() {
cin >> n;
int sum = 0; // 记录所有物品的质量总和
for (int i = 0; i < n; i++) {
cin >> matter[i];
sum += matter[i];
}
sort(matter, matter + n);
// 总和小于40或者最大的已经大于40了
if (sum < 40 || matter[0] > 40) {
cout << kind << endl;
return 0;
} else {
memset(visit, false, sizeof(visit));
DFS(0, 0);
cout << kind << endl;
}
return 0;
}
法三:动态规划
/**
* 用滚动数组法,每次状态转移仅仅根据前一行数组决定,故选择用一维数组
* 如果dp[j]已经有了x种方法,而dp[j-volume[i]]也有y种方法
* 那么由于物体i的存在,对于体积为j的口袋,又能多出dp[j-volume[i]]种方法
* 例如j=20,volume=10,而有5种方法能装满体积为10的口袋,那么只要有体积为10的物体就一定多出5种装满体积为20的口袋的方法
* 显而易见,当口袋体积等于该物体体积时,必然有一种方法
* /而可能不止一个物体有这样的体积,所以每次碰到这个体积的物体,就做一次++运算
*/
#include<cstdio>
int volume[21];//对于所有物品进行遍历,该物体体积为volume[i]
int dp[41];//dp[i]指口袋体积为i时,共有几种方法
int main() {
int n;
while (scanf("%d", &n) != EOF) {
for (int i = 1; i <= n; i++)//输入物品的体积
{
scanf("%d", &volume[i]);
}
for (int i = 1; i <= n; i++)//对于所有物品进行遍历,该物体体积为volume[i]
{
for (int j = 40; j >= volume[i]; j--)//j>=volume[i]因为dp[j]只能从0~40-volume[i]的状态转移而来
{
dp[j] += dp[j - volume[i]];
}
dp[volume[i]]++;
}
printf("%d\n", dp[40]);
}
return 0;
}
KY86 八皇后
题目描述
会下国际象棋的人都很清楚:皇后可以在横、竖、斜线上不限步数地吃掉其他棋子。如何将8个皇后放在棋盘上(有8 * 8个方格),使它们谁也不能被吃掉!这就是著名的八皇后问题。 对于某个满足要求的8皇后的摆放方法,定义一个皇后串a与之对应,即a=b1b2...b8,其中bi为相应摆法中第i行皇后所处的列数。已经知道8皇后问题一共有92组解(即92个不同的皇后串)。 给出一个数b,要求输出第b个串。串的比较是这样的:皇后串x置于皇后串y之前,当且仅当将x视为整数时比y小。
输入描述:
每组测试数据占1行,包括一个正整数b(1 <= b <= 92)
输出描述:
输出有n行,每行输出对应一个输入。输出应是一个正整数,是对应于b的皇后串。
输入
2
1
92
输出
15863724
84136275
代码
/**
* 非常朴素的八皇后问题,问题规模也已经框定好了,
* 只要把每次得到的列号转化成要比较的十进制数字即可
*/
#include<cstdio>
#include<algorithm>
#include<cmath>
#include<vector>
using namespace std;
vector<int> solut; //用来放最终的结果
int position[9]; //行号从1开始,其中下标代表行号,其中存放的内容代表列号
void DFS(int row) //row代表要放入的行号,逐行放入,因为要用的是列号,而且按照习惯都是一列一列计算的
{
if (row == 9) //row==9意味着从1~8行全都放入,已完成解
{
int temp = 0;
for (int i = 1; i <= 8; i++) {
temp += position[i] * pow(10, 8 - i); //非常朴素的计算方法_(:з」∠)_
}
solut.push_back(temp); //把得到的solution放进vector
} else {
for (int i = 1; i <= 8; i++) {
position[row] = i; //i在这里代表列号
bool flag = true; //用一个标志位来标记,是否冲突
for (int j = 1; j < row; j++) {
if (position[row] == position[j] || row - position[row] == j - position[j] ||
row + position[row] == j + position[j]) {
flag = false;
break;
} //这里的判断条件j - position[j]会把同一主对角线标记为同一个数字,与row - position[row]同时计算就能判断是否冲突
}
if (flag)
DFS(row + 1);
}
}
}
int main() {
DFS(1); //直接从第一行开始放
sort(solut.begin(), solut.end()); //这里应该不用sort因为得到的solution应该都是从小到大
int n;
while (scanf("%d", &n) != EOF) {
printf("%d\n", solut[n - 1]); //因为vector是从0开始的
}
return 0;
}