连接:http://bailian.openjudge.cn/practice/4124
题意:从1到n走过所有点恰好一次最短时间。乱搞的话会完美的超时(阶乘级别的复杂度,虽然范围很小,但是也足够超时了)。
思路:先想一个不太成熟的思路。用dp[j][s]表示。s记录的是每个点是否被走过的状态。而dp[s][j]表示的是从1走到j状态所用的最小时间。这样的思路成不成立呢?首先,考虑初始值。开始是在1号点,那么dp[1][1]自然就是0了,其他就是max;另外,题面说只要遍历每一个点,而于顺序的话,并没有要求,也就是说,通过任何一种演变方式到达该状态的方式都是等价的,满足了动态规划无后效性的要求。同时,对于每一个时刻,当前位置存储的值都是当前最优解,既问题具有最优子结构性质。同时,对于每一个演变,我们可以在dp[s][j]的基础上,推出当前状态的值可以通过上一步演变就到达的状态进行更新,这也就是所谓“人人为我”的过程。dp方程也好想,既:
dp[i][k] = min(dp[i][k],dp[i_pre][k_pre]+G[i_pre][i]);
接下来就是比较重要的问题了:如何表示这些状态,以及进行状态之间的计算呢?
用16个bool?太麻烦了!换一种思路:因为只有16个数,将他们编成二进制编码101010101010100……每一位代表当前位置所代表的点是否被走过,这样的话,只需要2^16个无符号shortint(实际用int)就可以表示所有可能的状态啦。
第一个问题是解决了,可如何进行状态间的变换呢?请把c语言程序设计翻到xxx页,有关位运算的章节:
&, 这个东西叫按位与,既每一位依次比较一样就是1,不一样就是0。平时常用的判断奇偶性的n&1就是最简单的应用。
|,这东西叫按位或,键位有点怪,一般在enter附近,意思是每一位依次比较有1就是1,全0就是0
^,按位异或,也是中文输入法下省略号的打法。官方的话是相同为0,不同为1,我的理解就是不带进位的加法。
~,取反,在tab上面,int下的话就是~x = -x-1 最常用的那个while(~scanf)用的就是这个原理(~0 = -0-1 = -1 = EOF)。
<< >> 左移右移 不多说,乘2除2
一些比较清奇的用法
从低位到高位,取n的第m位
return (n >> (m-1)) & 1;
从低位到高位.将n的第m位置1
return n | (1 << (m-1));
从低位到高位,将n的第m位置0
return n & ~(1 << (m-1));
(x&-x)只保留最低位的1
具体用法的话读代码,体会一下:
#include <iostream> #include <cstdio> #include <cstring> #include <algorithm> using namespace std; void read(){ #ifndef ONLINE_JUDGE freopen("D:\fengyu\Jiang_C\.vscode\in.txt","r",stdin); freopen("D:\fengyu\Jiang_C\.vscode\out.txt","w",stdout); #endif } int G[20][20]; int dp[20][(1<<16)+5]; int main() { read(); int n; while (cin >> n) { memset(dp,0x3f,sizeof(dp)); for (int i = 1; i <= n; i++) { for (int j = 1; j <= n; j++) { cin >> G[i][j]; } } dp[1][1] = 0; int ans = (1<<n)-1; for (int k = 1; k <= ans; k++) { for (int i = 1,_i = 1; i <= n; i++,_i<<=1) { if (k&_i) for (int j = 1, _j = 1; j <= n; j++,_j<<=1) { if (i!=j && k&_j) dp[i][k] = min(dp[i][k],dp[j][k^_i]+G[j][i]); } } } cout << dp[n][ans] << endl; } return 0; }