每周一算法:状态压缩动态规划

状态压缩

状态压缩是指用二进制表示集合的方式对状态进行压缩,将其表示为一个整数。

例如:用二进制表示一个集合的子集。

集合 S = { 1 , 2 , 3 , 4 , 5 } S = \{ 1,2,3,4,5\} S={1,2,3,4,5},那么二进制数 ( 01001 ) 2 (01001)_2 (01001)2,表示的就是 S S S的一个子集 S ′ = { 1 , 4 } S' = \{ 1,4\} S={1,4},该子集可以用一个十进制数 9 9 9来表示。

状态压缩动态规划

状态压缩动态规划其实是动态规划类问题中一种特殊的状态表示方式。若要表示的集合元素数量比较少(不超过 20 20 20)时,想要存储每个元素取或者不取时,可以借助位运算将状态压缩。需要借助状态压缩实现状态表示的动态规划问题就称为状态压缩动态规划。

例如取若干元素,如果选择的话对应位置记为 1 1 1,其余位置记为 0 0 0。例如一共有5个元素 a , b , c , d , e a,b,c,d,e a,b,c,d,e,分别用 1 , 2 , 4 , 8 , 16 1,2,4,8,16 1,2,4,8,16表示这5个元素,则集合 { a , c } \{ a,c\} {a,c}可以用 ( 00101 ) 2 = 3 (00101)_2 = 3 (00101)2=3来表示,而集合 { b , d , e } \{ b,d,e\} {b,d,e}可以用 ( 11010 ) 2 = 26 (11010)_2=26 (11010)2=26表示。

对于元素个数为 n n n的情况,其空间复杂度为 ( 2 n ) (2^n) (2n)

问题应用

n n n个人在做传递物品的游戏,编号为 1 1 1- n n n

游戏规则是这样的:开始时物品可以在任意一人手上,他可把物品传递给其他人中的任意一位;下一个人可以传递给未接过物品的任意一人。

即物品只能经过同一个人一次,而且每次传递过程都有一个代价;不同的人传给不同的人的代价值之间没有联系;

求当物品经过所有 n n n个人后,整个过程的总代价是多少。

输入描述

第一行为 n n n,表示共有 n n n个人( 2 ≤ n ≤ 16 2≤n≤16 2n16)。
以下为 n × n n×n n×n的矩阵,第 i + 1 i+1 i+1行、第 j j j列表示物品从编号为 i i i的人传递到编号为 j j j的人所花费的代价,特别的有第 i + 1 i+1 i+1行、第 i i i列为 − 1 -1 1(因为物品不能自己传给自己),其他数据均为正整数( < = 1000 <=1000 <=1000)。

输出描述

一个数,为最小的代价总和。

输入样例

3
-1 2 4
3 -1 5
4 4 -1

输出样例

6

算法思想

在传递过程中,当前状态可以表示为哪些人已经传递过物品、并且物品目前传递到哪个人手上。而已经传递过物品的人可以理解为从集合中已选取了哪些元素,因此可以使用状态压缩的方式,用二进制数01来表示未传递和已传递的两种状态。

状态表示

f[s][j] 表示当前传递状态的集合为s、并且传递到编号为j的人手中时的最小的代价总和。

状态计算

物品需要从上一个人(不妨设编号为i)传递过来,因此需要枚举所有可以传递到j的人,取其中最小的代价总和。可以传递到j,意味着编号为i的人已经包含在集合s中。

状态转移方程: f [ s ] [ j ] = min ⁡ { f [ s − { j } ] [ i ] } + w [ i ] [ j ] f[s][j] = \min\{f[s - \{j\}][i]\} + w[i][j] f[s][j]=min{f[s{j}][i]}+w[i][j]

其中: f [ s − { j } ] [ i ] f[s - \{j\}][i] f[s{j}][i]表示集合s中不包含j,即编号j的人尚未被传递过物品。

初始状态

题目中求最小值,因此首先将f数组初始化为无穷大。其次,从自己出发且传递给自己时,最小代价为0,即f[1 << i][i] = 0

时间复杂度

状态数 2 n × n 2^n \times n 2n×n,转移过程要循环 n n n次,所以时间复杂度为 O ( 2 n × n 2 ) O(2^n\times n^2) O(2n×n2)

代码实现

#include <iostream>
#include <cstring>
using namespace std;
const int N = 16, INF = 0x3f3f3f3f;
int w[N][N], f[1 << N][N];
int main()
{
    int n;
    cin >> n;
    
    for(int i = 0; i < n; i ++)
        for(int j = 0; j < n; j ++)
            cin >> w[i][j];
    
    //求最小值,因此将f数组初始化为无穷大
    memset(f, 0x3f, sizeof f);    
    //初始状态,从自己出发传递时,最小代价为0
    for(int i = 0; i < n; i ++)
        f[1 << i][i] = 0;
    
    //枚举所有可能的传递状态
    for(int s = 0; s < (1 << n); s ++)
    {
        //枚举上一个传递过来的人
        for(int i = 0; i < n; i ++)
        {
            //如果i在集合s中,则可以从i传递到j
            if(s >> i & 1) 
            {
                //枚举当前要传递到的人
                for(int j = 0; j < n; j ++)
                {
                    //如果j在集合i中
                    if(s >> j & 1)
                    {
                        //状态转移,求出从i传递到j的最小值
                        f[s][j] = min(f[s][j], f[s - (1 << j)][i] + w[i][j]);
                    }
                }
            }
        }
    }    
    int res = INF;
    //打擂台求出,传递过所有人后,且最后在i手上时的最小值
    for(int i = 0; i < n; i ++)
    {
        res = min(res, f[(1 << n) - 1][i]);
    }
    cout << res << endl;   
    return 0;
}
  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

少儿编程乔老师

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值