动态规划:状态压缩DP入门(两道例题c++)

文章目录

  • 糖果
  • 旅行商问题

糖果

题目传送门

糖果店的老板一共有 M M M 种口味的糖果出售。为了方便描述,我们将 M M M 种口味编号 1 1 1 M M M
小明希望能品尝到所有口味的糖果。遗憾的是老板并不单独出售糖果,而是 K K K 颗一包整包出售。
幸好糖果包装上注明了其中 K K K 颗糖果的口味,所以小明可以在买之前就知道每包内的糖果口味。
给定 N N N 包糖果,请你计算小明最少买几包,就可以品尝到所有口味的糖果。


这是道入门的状态压缩DP的题目。

一个int类型的数据是四个字节,可以表示32位二进制数

一个long long类型的数据是八个字节,可以表示64位二进制数

二进制数只有01的不同,因此对于五位二进制数: 00000 – 11111 的不同组合就是 2^5 种,而我们便可以把每一种组合表示为一个状态,比如00001 00010,他们都是不同的状态。

我们把每一个糖果包中的 k颗糖果 表示为一个状态:例如:

  • 1 1 2: 00011
  • 1 2 3: 00111
  • 2 3 5: 10110
  • 5 1 2: 10011

如上,我们把每一个状态压缩为了二进制数字,其中00011表示了 这包糖果具有 1 1 2这个口味组合, 10110表示了这包糖果具有2 3 5这个口味组合。

那么题目让我们品尝到所有口味的糖果.

显而易见,我们就必须得到一个 11111 的口味组合的状态,使得 1 2 3 4 5 这m种口味都能够表示。


那么我们首先定义一个 kw[i]存储给出的第 i i i 包,每包 k w [ i ] kw[i] kw[i] 颗糖果的口味状态

即我们转换为

  • kw[1] =00011:第一包糖果的口味状态为 00011
  • kw[2] =00111:第二包糖果的口味状态为 00111
  • kw[6] =10011 :第三包糖果的口味状态为 10011

我们定义dp数组:其中我们的dp数组应该能表示所有的口味状态,即 11111…111 有m个1,因此我们的dp数组应该足够大,对于题目中m最大为20,所以我们应该定义:

dp[1<<20] 使得最多可以表示20个1,因此能够存储所有的状态。

  • d p [ i ] dp[i] dp[i]:表示得到口味为 i i i 所需要的糖果包的最少的数量

  • 状态转移:我们当前的口味组合为 i i i,则我们加入一包糖果,得到新的口味的组合为 j j j,则从 i i i j j j 需要的糖果包的数量就是 d p [ i ] + 1 dp[i] +1 dp[i]+1,如果说已经已经更新过了 d p [ j ] dp[j] dp[j],即 j j j 种口味的糖果包的最少数量,则如果 d p [ j ] > d p [ i ] + 1 dp [j] >dp[i]+1 dp[j]>dp[i]+1,则我们将dp[j] 更新为这个较小的值

状态转移公式为:
d p [ j ] = m i n ( d p [ j ] , d p [ i ] + 1 ) dp[j]=min(dp[j],dp[i]+1) dp[j]=min(dp[j],dp[i]+1)

#include 
#include 
#include 
#include 
using namespace std;

//TODO: Write code here
int n,m,k;
const int N=1e5+10;
int nums[N],dp[1<<20],kw[N];
signed main()
{
	cin>>n>>m>>k;
    /*
    dp[i]表示口味为i时最少的糖果包的数量
    */
    memset(dp,-1,sizeof(dp));
    for (int i=1;i<=n;i++)
    {
        int temp=0;
        for (int j=1;j<=k;j++)
        {
            int a;
            cin>>a;
            temp|=(1<<a-1); 
            /*
        	记录每一包糖果的口味状态 
            a=1:  temp=00001
			a=2:  temp=00011
			a=3:  temp=00111
            */
        }//temp表示第i包糖果的口味
        kw[i]=temp; //第i包糖果的口味
        dp[temp]=1; //口味为temp时需要的最少的糖果包的数量默认为1
    }
    for (int i=0;i<1<<m;i++)//遍历所有的口味组合的状态
    {
        if (dp[i]!=-1)//存在这种口味组合
        {
            for (int j=1;j<=n;j++)
            {
                int temp=kw[j];//获得每一包糖果的口味
                //i|temp表示原来的i口味加上这包temp口味后得到的 新的口味
                if (dp[i|temp]==-1){
                	dp[i|temp]=dp[i]+1;
				}
                dp[i|temp]=min(dp[i|temp],dp[i]+1);
                
            }
        }
    }
    cout<<dp[(1<<m)-1];//得到所有的口味的最少糖果数量
#define one 1
	return 0;
}

旅行商问题

题目传送门

在一个二维平面中,有 n n n 个坐标点。一个人 ( 0 , 0 ) (0,0) (0,0) ( 0 , 0 ) (0,0) (0,0) 点处出发去达所有点,问至少要走多少距离?

我们可以把整个地图看作一个 S S S 集合, S S S 集合包含原点以及所给的坐标点。

题目让我们要从S中的 ( 0 , 0 ) (0,0) (00) 开始走,能够到达所有点,求这个最短路径。

我们不妨设: d p [ S ] [ j ] dp [S] [j] dp[S][j]为 在集合S中走完所有的点,并且以j为最后的终点的最短距离

那么如果我们在 S S S 集合中把 这个点 j j j 去掉,即现在变成了 S − j S-j Sj 的集合,那么现在 S S S集合中就不包含 j j j 点,我们假设 d p [ S − j ] [ k ] dp[S-j][k] dp[Sj][k] 为在 S − j S-j Sj 集合中以 k k k 为终点的最短距离,那么只要我们得到了这个dp值,并且我们再加上 d i s ( j , k ) dis(j,k) dis(j,k) 之间的距离,因为两点之间的距离肯定是最短的,无法再分割了.

因此我们便可以在去掉j点的S集合中寻找一个k点,然后计算j点和k点的最短距离来得到最短的距离。

我们的状态转移方程如下:

d p [ S ] [ j ] = m i n ( d p [ S ] [ j ] , d p [ S − j ] [ k ] + d i s ( j , k ) ) dp[S][j]=min(dp[S][j],dp[S-j][k]+dis(j,k)) dp[S][j]=min(dp[S][j],dp[Sj][k]+dis(j,k))

状态转移方程的分析应该是比较简单的,那么我们考虑如何来表示 S集合??

我们使用二进制数的方式来表示,比如 S的集合可以表示为 11111,他的含义是所有的点都在S集合中,S-j的集合可以表示为 11011,他的含义是去掉了j点(用二进制0表示这个点)


实现的方式:

  • 在dp[S] [j]的状态转移方程中,因为我们使用了dp[S] [j],所以我们首先要判断S集合中是否含有 j 这个点
    • (S>>j)&1:由这个式子便可以判断S集合中是否包含 j 点。
  • 我们要使用dp[S-j] [k],因此我们要把 j 点从S集合中去除,同时我们要实现枚举 S-j 中的所有的点
    • S ^(1<:即可实现把 j 点从S集合中去除。
    • S ^ (1<> k &1:即可实现在 S-j 集合中,通过枚举一个变量k,来实现遍历 S-j 中的所有的点,因为这些点在二进制中一定是 1,所以要 & 1

我们的dp应该定义为 dp[1<<17] [20],因为 S的集合最多有15个点,所以我们的 第一维应该足够容纳 11111…1111等最多 15 个1,第二维是作为终点的点的个数


综上,我们首先for循环枚举整个S集合的所有可能的情况,然后for循环枚举 j ,表示要将 j 从S中移除,然后for循环枚举 k,利用k 来实现遍历 S-j 集合中的每一个点了。

最后我们来枚举一个最小值,我们 的 dp[S] [0]表示在集合S中到达终点0的最短路径, 我们通过一个for循环来遍历在S中所有的以 i 为终点的最短路径,即在所有的 dp[(1<

//坐标搜索:旅行商问题
namespace test47
{
	const int N = 1005;
	int n;
	double dp[1 << 17][20];	//表示地图的点的集合
	double  x[N], y[N];
	double dis(int a, int b)
	{
		return sqrt((x[a] - x[b]) * (x[a] - x[b]) + (y[a] - y[b]) * (y[a] - y[b]));
	}
	void test()
	{
		memset(dp, 0x7f, sizeof(dp));
		cin >> n;//坐标点的数量
		x[0] = 0, y[0] = 0;
		for (int i = 1; i <= n; i++)
		{
			cin >> x[i] >> y[i];
		}
		++n;	//原点(0,0)
		/*
		dp[i][j]表示在 i 集合中到达点 j 的最短路径长度
			dp[i][j]=min(dp[i-j][k]+dis(j,k),dp[i][j]) 在i-j集合中到达k的最短路径长度+j与k的路径长度
		*/
		dp[1][0] = 0;//一开始S集合中只有(0,0),距离为0
		for (int S = 0; S < (1 << n); S++)//遍历所有的地图集合
		{
			for (int j = 0; j < n; j++)//枚举点j,改变集合S为 S-j
			{ 
				for (int k = 0; k < n; k++)//枚举到达j的点k,k属于集合S-j
				{
					/*
					1. 判断当前集合S中是否含有j点:(S<
					if (((S >> j) & 1) && ((S ^ (1 << j)) >> k & 1))
					{
						dp[S][j] = min(dp[S ^ (1 << j)][k] + dis(k, j), dp[S][j]);
					}
				}
			}
		}
		double ans = dp[(1 << n) - 1][0];//找到最短路径
		for (int i = 1; i < n; i++)
		{
			ans= min(dp[(1 << n) - 1][i]);//在整个集合中,以 i 为终点的最短距离
		}
		printf("%.2lf", ans);
	}
}

你可能感兴趣的:(动态规划,算法题解,动态规划,c++,算法)