题解 P1171 【售货员的难题】
dzz1537568241 · · 题解
写这份题解,是想要作为 状压 + dp的入门指南
一想起我刚学状压的时候,怎一个苦字了得
为了防止有人步入我的后尘,所以这篇题解主要是为了扫盲,各位吃瓜的大佬可以走了
一,二进制运算
[洛谷日报第79期]二进制与位运算
这个实在太重要了,先把二进制运算搞懂了,再来搞状压
二,状压是什么?
众所周知,OI是一个搞数学的学课,所谓状压,就是把一个状态表示成一个集合,再把集合表示成二进制数
总的来说就有这样一个过程
状态 - > 集合 - > 二进制
在进入具体操作之前,我想先和各位讨论一下,为什么要把集合转成二进制数呢?
二进制,这是一个美妙的进制数,0/1,表达了 有 或者 没有
而使用两进制数来描述集合,意味着集合的这个元素 有 或者 没有
比如集合S,有4个元素
1111 这个两进制数就能够表达集合 s
那如果要表达集合s,去除第二个元素,这样的子集怎么表示呢?
1011,而去除掉第二个 和 第三个元素的二进制数, 是1001
推而广之,如果全集共有 N 个元素,
去除掉 第a1, 第a2, 第a3... 第an号元素的子集,
就是把 N个1构成的两进制数的所有的{an}位上的1,变成0
- 那么往集合中加入元素呢
向一个集合 s 中加入第 i 号元素,
就是把两进制数的第 i 位从 0 变为 1
- 删除元素不用我说了吧?
向一个集合 s 中删除入第 i 号元素,
就是把两进制数的第 i 位从 1 变为 0
下面是整理的一些两进制上的常用操作(我刚刚给你们的链接上有详细的解析,这里只是总集)
//集合 s 代表当前集合
//右数 k 位二进制数 0/1 代表 第 k 号元素 不在集合 / 在集合 里
//必备操作1:
//将第k个元素加入集合
s | (1 << (k - 1));
//必备操作2:
//将第k个元素从集合中删除
s & (~ (1 << (k - 1)) );
//必备操作3:
//1)如果第k个元素在 集合s里, 删除它
//2)如果不在,把它加入集合s
//可以想象成一盏灯的开关:
//按下开关, 明 -> 暗, 暗 -> 明
s ^ (1 << (k - 1));
//必备操作4:
//判断元素k 是否在集合s里
//在:返回1,不在:返回0
(s >> (k - 2)) & 1 == 1
三,怎么建模?
step1 : 怎么用集合描述状态?
以这道题目为例,什么是状态?
一个城市是否已经被走过了可以作为状态,这样就可以把 走过的城市作为一个集合,表示成二进制数
step2 : 怎么dp?
这个....枚举就好。
#include <algorithm>
#include <cstring>
#include <cstdio>
#include <iostream>
using namespace std;
const int maxn = 450;
int rd(){
int x = 0, f = 1; char c = getchar();
while(c < '0' || c > '9'){if(c == '-'){f = - 1;} c = getchar();}
while(c >= '0' && c <= '9'){x = x*10 + c - '0'; c = getchar();}
return x * f;
}
int N, c[maxn][maxn], f[1 << 20][20];
int dp(){
//1 << N代表全集
memset(f,0x3f3f3f,sizeof(f));
f[(1 << N) - 1][0] = 0;
for(int s = (1 << N) - 2; s >= 0 ; s--){
for(int v = 0; v < N ; v++){
//枚举当前集合,售货员处在的位置
for(int u = 0; u < N; u++){
if( (s >> u) & 1)continue;//说明已经到过u这个节点了
f[s][v] = min(f[s][v], f[s|(1 << u)][u] + c[v][u]);
}
}
}
}
int main(){
N = rd();
for(int i = 0; i < N; i ++){
for(int j = 0; j < N; j++){
c[i][j] = rd();
}
}
dp();
cout<<dp[0][0];
}