[状态压缩 & 回溯搜索] 海贼王之伟大航路

描述

“我是要成为海贼王的男人!”,路飞一边喊着这样的口号,一边和他的伙伴们一起踏上了伟大航路的艰险历程。

路飞他们伟大航路行程的起点是罗格镇,终点是拉夫德鲁(那里藏匿着“唯一的大秘宝”——ONE PIECE)。而航程中间,则是各式各样的岛屿。

因为伟大航路上的气候十分异常,所以来往任意两个岛屿之间的时间差别很大,从A岛到B岛可能需要1天,而从B岛到A岛则可能需要1年。当然,任意两个岛之间的航行时间虽然差别很大,但都是已知的。

现在假设路飞一行从罗格镇(起点)出发,遍历伟大航路中间所有的岛屿(但是已经经过的岛屿不能再次经过),最后到达拉夫德鲁(终点)。假设他们在岛上不作任何的停留,请问,他们最少需要花费多少时间才能到达终点?

输入

输入数据包含多行。
第一行包含一个整数N(2 < N ≤ 16),代表伟大航路上一共有N个岛屿(包含起点的罗格镇和终点的拉夫德鲁)。其中,起点的编号为1,终点的编号为N。
之后的N行每一行包含N个整数,其中,第i(1 ≤ i ≤ N)行的第j(1 ≤ j ≤ N)个整数代表从第i个岛屿出发到第j个岛屿需要的时间t(0 < t < 10000)。第i行第i个整数为0。

输出

输出为一个整数,代表路飞一行从起点遍历所有中间岛屿(不重复)之后到达终点所需要的最少的时间。

样例输入

样例输入1:
4
0 10 20 999
5 0 90 30
99 50 0 10
999 1 2 0

样例输入2:
5
0 18 13 98 8
89 0 45 78 43 
22 38 0 96 12
68 19 29 0 52
95 83 21 24 0

样例输出

样例输出1:
100

样例输出2:
137

提示

提示:
对于样例输入1:路飞选择从起点岛屿1出发,依次经过岛屿3,岛屿2,最后到达终点岛屿4。花费时间为20+50+30=100。
对于样例输入2:可能的路径及总时间为:
1,2,3,4,5: 18+45+96+52=211
1,2,4,3,5: 18+78+29+12=137
1,3,2,4,5: 13+38+78+52=181
1,3,4,2,5: 13+96+19+43=171
1,4,2,3,5: 98+19+45+12=174
1,4,3,2,5: 98+29+38+43=208
所以最短的时间花费为137
单纯的枚举在N=16时需要14!次运算,一定会超时。

解题分析

这题真的是超级经典的状态压缩 和 深搜 和 递归回溯 和位操作和减枝。首先我们可以发现,这题带有一点图思想,节点之间都有航线连接,但是边上的值是带有方向的,从A到B和从B到A花的时间是不一样的,那么,我们先去思考一下,直接去暴力搜索的话,我们应该怎么搜?题目的意思大概是要满足每一个中间的节点都必须被访问过,而且顺序是任意的,中间节点最多有14个,所以数量级在14!= 87178291200,显然直接爆炸,这是不可取的。这个时候,我们就不得不引入我们的减枝策略和二进制状态压缩策略了。我们存储每次的中间状态的时间值,就是已经经过了哪些城市,然后最后到达的是哪个城市,就是一个二维的状态表达吧,我们把这个时间记录下来。接着,我们还必须确定我们减枝的一个正确性。我们先弄一个深搜DFS的函数,传入两个变量吧,一个是当前我们所在的城市,另一个是我们已经经过的城市的数量。接着,由于是深搜,我们就可以用一些全局变量去帮助我们记录状态,当然你必须注意,这样的话你必须在这次搜索结束回溯的时候,重置状态,防止混乱和错误。然后,如果说我们已经在N-1个城市了,我们直接润去最后一个城市,因为这个时候我们已经经历了全部中间城市节点,这个也是我们深搜的一个边界判断吧。接着,我们去遍历中间的N-2个城市,并且我们设定相应的二进制位的位置为某个城市。然后,这里强调一下位运算吧,我们用右移>>和&1操作,取出state中二进制上某一位,判断是否为1来检查去没去过;我们用左移1<<和|操作,去把state中某一个位设置为1; 我们用左移1<<和^操作(0异或0还是0,0异或1还是1),把state中某一个位从1设置回0。这样我们就可以正确回溯,也可以优雅地进行位操作啦。这里的剪枝操作是很精髓的,首先,如果我们这个当前到我们想去的下一个地方的总时间都已经大于等于最佳时间了,直接跳过,然后,我们之前设置的中间状态时间数组也起了重要的作用,如果在之前经历的城市都一毛一样,而且,下一个想去的地方也一样的情况下,现在我们的中间时间要大于等于之前我们经历的中间时间,我们直接剪枝。这个剪枝简直完美,直接把时间的数量级降下来了。最后输出答案即可。

代码实现
#include <iostream>
#include <cmath>
//#include <iomanip>
#include <string>
#include <cstring>
#include <cstdio>
#include <algorithm>
#include <vector>
#include <map>
#include <set>
#include <unordered_map>
#include <unordered_set>
#include <list>
#include <bitset>
#include <queue>
#include <stack>
#include <cstdlib>
#define INF (1<<30)
using namespace std;

int N;
int boardTime[20][20];
int midTime[1<<15][17];
int curTime=0;
int bestTime=INF;
int state=0;

void dfs(int k,int step){
	if(step==N-1){
		bestTime=min(bestTime,curTime+boardTime[k][N]);
		return;
	}
	for(int i=2;i<=N-1;i++){
		if((state>>(i-2))&1 || curTime+boardTime[k][i] >=bestTime || midTime[state | (1<<(i-2))][i] <= curTime+boardTime[k][i]) continue;
		state |= 1<<(i-2);
		curTime+=boardTime[k][i];
		midTime[state][i]=curTime;
		dfs(i,step+1);
		state ^= 1<<(i-2);
		curTime-=boardTime[k][i];
	}
}


int main(){
	scanf("%d",&N);
	
	for(int i=1;i<=N;i++)
		for(int j=1;j<=N;j++){
			scanf("%d",&boardTime[i][j]);
		}
	
	for(int i=1;i<(1<<(N-2));i++)
		for(int j=1;j<=N-1;j++){
			midTime[i][j]=INF;
		}
	
	dfs(1,1);
	printf("%d\n",bestTime);
	return 0;
}

  • 23
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值