【C++ 状态压缩DP初探】详细原理 ,经典TSP题目的递归写法与递推写法,附赠Poj一题状压

C++ 状态压缩DP初探

    • 前置知识
      • 状态压缩
      • DP
    • 状压DP初探
      • TSP 问题
        • 递归式代码
        • 递推式代码
      • Travelling by Stagecoach 问题

前置知识

状态压缩

即使用二进制表示一个集合S。
上一篇就是讲二进制枚举的,实现过程就是状压啦!
【C++ 枚举问题入门】两个难度规模,N从20 -> 40

DP

即Dynamic Programing ,动态规划
这类DP问题的性质:
最优子结构,问题与子问题有相似性,无后效性。

动态规划一般可分为线性动规,区域动规,树形动规,背包动规四类。
由前一个状态经过状态转移方程之后推到后一个状态。

状压DP初探

问:【什么问题才是状压DP?】
答:针对、利用集合的DP问题

问:【集合怎么表示】?
答:使用二进制表示。(见枚举入门的原理)

\ 原理 /
我们要从N个数字选择k个,那我们可以类比到二进制中去。
比如当N=3时,所有的二进制排列方案为:
如果我们看第i位,如果val[i]=1则表示我们选择i号元素;为0我们则不选
000 都不选
001 选择第三个元素
010 选择第二个元素
011 选择第二、第三个元素
100 选择第一个元素
101 选择第一、第三个元素
110 选择第一、第二个元素
111 都选
可以看到,十进制从0到2N-1 我们就把所有可能的情况都列举起来了。

问:【集合怎么添加与删除元素】?
答:使用位运算。如果我们已经有一个集合S
S={10010(b)}
我们可以看到S有2、5号元素【这里我们选择从右往左看,问题不大】
添加元素u(表示添加第u号元素)
S∪{u} = S | 1 << u
删除元素u(表示删除第u号元素)
S \ {u} = S & ~(1 << u)

当然,集合的位运算有很多,但是这里我们先了解这两个就暂时足够了。

问:【这个状压DP怎么用?】
答:状压原理知道了(集合二进制表示),但是这个怎么和DP打起组合拳呢?这个根据不同题目的不同状态转移方程不同,写法自然也有所不同了。在这里我们先引入一道最经典的题目。

TSP 问题

TSP ,是指 Traveling Salesman Problem ,是NP困难问题。
如果直接枚举所有路线时间复杂度:O((N-1)!),无能为力呀。
但是对于题目规模比较小的话,我们可以利用状压DP轻松(并不!)完成。

【旅行商问题】
给定一个由n个顶点组成的带权有向图的距离矩阵。
要求从顶点0除法,经过每个顶点恰好一次后再回到顶点0。
问所经过的边的总权重的最小值是多少。
数据规模 2<=n<=15

说白就是一个图每个点都经过一次,从原点出发回到原点,求路径长度的最小值。看图!
【C++ 状态压缩DP初探】详细原理 ,经典TSP题目的递归写法与递推写法,附赠Poj一题状压_第1张图片
最短路径为25(0->2->4->1->3->0)

我们定义【状态】
dp[S][v] =k
S 表示当前已经访问过的点的集合
v 表示现在访问的顶点
k 表示已经访问了S,现在在v点,现在继续访问剩下顶点并回到原点的路径权重总和的最小值
【注意:这里的k是反向的,终状态为0,起状态大】

然后再来搞一下【状态转移方程】

【C++ 状态压缩DP初探】详细原理 ,经典TSP题目的递归写法与递推写法,附赠Poj一题状压_第2张图片
dp[V][0]=0 ,如果都访问过了且现在在原点,那接下来不需要走了,k为0

dp[S][v]=min⁡{dp[S∪{u} ][u]+d[v][u]|u∉S},表示:
在余下的没访问中的顶点u中选择dp[S∪{u} ][u]+d[v][u]中的最小的值
因为如果现在是[S][v],选择了u之后集合增加元素u,S变成S∪{u} ,并且当前访问的顶点变成了u。当然你要走过从v到u之间的距离才行对吧。

(DP递推一般都挺复杂的,细品!)
时间复杂度:O(2N N2)
接下来就是代码部分了!我们有两种代码写法。

递归式代码

int n;
int d[MAX][MAX];		///存距离矩阵
int dp[1<<MAX][MAX];	///存dp数组
int rec(int S,int v){
    if(dp[S][v]>=0)return dp[S][v];	///记忆化递归一下
    if(S==(1<<n)-1 && v==0)return dp[S][v]=0;///状态转移方程1
    int res=INF;
    for(int u=0;u<n;++u){
        if(!(S >> u & 1))///状态转移方程2
            res = min(res,rec(S|1 << u,u) + d[v][u]);
    }
    return dp[S][v]=res;
}
int main()
{
    ///IOS;
    int m;
    cin >> n >> m;
    for(int i=0;i<n;++i){	///初始化距离矩阵
        for(int j=0;j<n;++j){
            if(i!=j)d[i][j]=INF;
        }
    }
    memset(dp,-1,sizeof(dp));	///初始化dp数组
    while(m--){
        int a,b,c;
        cin >> a >> b >> c;
        d[a][b]=c;
    }
    cout << rec(0,0);
    return 0;
}

递推式代码

int n;
int d[MAX][MAX];
int dp[1<<MAX][MAX];
int main()
{
    ///IOS;
    int m;
    cin >> n >> m;
    for(int i=0;i<n;++i){
        for(int j=0;j<n;++j){
            if(i!=j)d[i][j]=INF;
        }
    }
    for(int S=0;S< 1<<n;++S)///dp初始化要搞成INF了
        fill(dp[S],dp[S]+n,INF);
    while(m--){
        int a,b,c;
        cin >> a >> b >> c;
        d[a][b]=c;
    }
    dp[(1<<n)-1][0]=0;///状态转移方程1
    for(int S = (1<<n)-2;S >= 0;S--){///枚举S
        for(int v=0;v<n;++v){///枚举v
            for(int u=0;u<n;++u){///枚举S的每一位,看看是否访问过了
                if(!(S >> u & 1))///状态转移方程2
                    dp[S][v]=min(dp[S][v],dp[S| 1 << u][u] + d[v][u]);
            }
        }
    }
    cout << dp[0][0];
    return 0;
}

Travelling by Stagecoach 问题

链接 POJ 2686
题目:

Once upon a time, there was a traveler.
He plans to travel using stagecoaches (horse wagons). His starting point and destination are fixed, but he cannot determine his route. Your job in this problem is to write a program which determines the route for him.
There are several cities in the country, and a road network connecting them. If there is a road between two cities, one can travel by a stagecoach from one of them to the other. A coach ticket is needed for a coach ride. The number of horses is specified in each of the tickets. Of course, with more horses, the coach runs faster.
At the starting point, the traveler has a number of coach tickets. By considering these tickets and the information on the road network, you should find the best possible route that takes him to the destination in the shortest time. The usage of coach tickets should be taken into account.
The following conditions are assumed.
A coach ride takes the traveler from one city to another directly connected by a road. In other words, on each arrival to a city, he must change the coach.
Only one ticket can be used for a coach ride between two cities directly connected by a road.
Each ticket can be used only once.
The time needed for a coach ride is the distance between two cities divided by the number of horses.
The time needed for the coach change should be ignored.

(太长不看)翻译:

给一个无向有权图,m个顶点。
旅行家想从a号顶点旅行到b号顶点。
他有n张车票,每张车票有ti匹马。
一张车票只能用一次,每条路只能使用一张车票。
如果两个城市之间花费时间为d,使用了一张有5匹马的车票,那么时间变为d/5
求旅行的最短时间。

思路:

车票设为一个集合S,一样先定义【状态】

dp[S][v] =k
S 表示剩下的车票集合
v 表示现在访问的顶点
k 表示还有车票集合S,现在在v点,从开始点到v点,车票还剩下S集合的所需时间的最小值
【注意:这里的k是正向的,原点0终点大】

再定义【状态转移方程】,与前面类似
只不过这次的集合是越来越小的。
在这里插入图片描述
第一个式子好理解。
第二个式子的前提S里面包含i号元素,也就是i号车票还没用掉。
那么对于相邻的v到u的路,我们枚举每一个还有的车票,看现在与之前递推的答案取最小的。

那么答案是什么?答案就是min{dp[S][b]},即表示到b点时还剩下车票集合S中所有情况的最小值。

我们使用递推式的代码:

dp[(1<<n)-1][a]=0;	///见上述递推1
double res = INF;
for(int S = (1<<n)-1;S >= 0;S--){
    res = min(res,dp[S][b]);	///取车票集合S,到达b点的时间最小值。
    for(int v=1;v<=m;++v){		///枚举车票在S下每一个城市作为当前点
        for(int i=0;i<n;++i){	///枚举S每一位(就是每一张车票)
            if((S >> i & 1)){	///表示当前位车票还没用过
                for(int u=1;u<=m;++u){	///枚举与v相邻的城市u
                    if(d[v][u]>=0){		///表示v u相邻
                        dp[S & ~(1 << i)][u] = min(dp[S & ~(1 << i)][u],dp[S][v] + (double)d[v][u]/t[i]);	///见上述递推2
                    }
                }
            }
        }
    }
}

哈哈,金字塔类型的for循环

完整AC代码!
POJ不让用万能头讨厌!

#include 
#include 
#include 
#define show(x) std::cerr << #x << "=" << x << std::endl
#define IOS ios_base::sync_with_stdio(false);cin.tie(NULL);cout.tie(NULL);
using namespace std;
typedef long long ll;
const int MAX_N=15;
const int MAX_M=35;

const double INF=1e12;
const double EPS = 0.001;
const ll MOD=1e9+7;
double t[MAX_N];				///每张车票的马数目
double d[MAX_M][MAX_M];			///城市距离时间的临界矩阵
double dp[1<<MAX_N][MAX_M];		///DP数组
int main()
{
    ///IOS;
    int n,m,a,b,k;
    while(cin >>n >> m >> k >> a >> b){
        if(n==0 && m==0)break;
        memset(t,0,sizeof(t));
        for(int i=0;i<n;++i)cin >> t[i];
        for(int i=1;i<=m;++i){
            for(int j=1;j<=m;++j){
                if(i!=j)d[i][j]=-1;		///距离设置成负的!不然会有大问题
            }
        }
        for(int S=0;S< 1<<n;++S)
            fill(dp[S],dp[S]+m+1,INF);
        while(k--){
            int aa,bb,cc;
            cin >> aa >> bb >> cc;
            d[aa][bb]=cc;
            d[bb][aa]=cc;
        }
        dp[(1<<n)-1][a]=0;
        double res = INF;
        for(int S = (1<<n)-1;S >= 0;S--){
            res = min(res,dp[S][b]);
            ///show(dp[S][b]);
            for(int v=1;v<=m;++v){
                for(int i=0;i<n;++i){
                    if((S >> i & 1)){
                        for(int u=1;u<=m;++u){
                            if(d[v][u]>=0){
                                dp[S & ~(1 << i)][u] = min(dp[S & ~(1 << i)][u],dp[S][v] + (double)d[v][u]/t[i]);
                                ///show(v);show(u);show(d[v][u]);
                            }
                        }
                    }
                }
            }
        }
        if(res ==INF){
            cout << "Impossible" << endl;
        }else cout << fixed << setprecision(3) << res << endl;
    }

    return 0;
}

【C++ 状态压缩DP初探】详细原理 ,经典TSP题目的递归写法与递推写法,附赠Poj一题状压_第3张图片
红绿灯都来一下!
(神TM输出Impossible我打成impossible了。。)

你可能感兴趣的:(【C++ 状态压缩DP初探】详细原理 ,经典TSP题目的递归写法与递推写法,附赠Poj一题状压)