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

文章目录

糖果

题目传送门

糖果店的老板一共有 �M 种口味的糖果出售。为了方便描述,我们将 �M 种口味编号 11 ∼ �M

小明希望能品尝到所有口味的糖果。遗憾的是老板并不单独出售糖果,而是 �K 颗一包整包出售。

幸好糖果包装上注明了其中 �K 颗糖果的口味,所以小明可以在买之前就知道每包内的糖果口味。

给定 �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]:存储给出的n包,每包k颗糖果的口味状态。

即我们转换为

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

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

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

  • 状态转移:我们当前的口味组合为 i,则我们加入一包糖果,得到新的口味的组合为 j,则从 i 到 j 需要的糖果包的数量就是 dp[i] +1,如果说已经已经表示过了dp[j],即 j 种口味的糖果包的最少数量,则如果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)

//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;
    int tot=(1<<m)-1;  //表示获得所有糖果的状态--> m个1
    /*
    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);
        }//temp表示第i包糖果的口味
        kw[i]=temp; //第i包糖果的口味
        dp[temp]=1; //口味为temp时需要的最少的糖果包的数量默认为1
    }
    for (int i=0;i<=tot;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]=dp[i]+1;
                }
            }
        }
    }
    cout<<dp[tot];//得到所有的口味的最少糖果数量
#define one 1
	return 0;
}

旅行商问题

题目传送门

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

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

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

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

那么如果我们在S集合中把 这个点 j 去掉,即现在变成了 S-j 的集合,那么现在S集合中就不包含 j 点,我们假设dp[S-j] [k] 为在S-j集合中以 k 为终点的最短距离,那么只要我们得到了这个dp值,并且我们再加上 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):即可实现把 j 点从S集合中去除。
    • S ^ (1<<j)>> 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<<n)-1] [i] 中寻找最小值。

//坐标搜索:旅行商问题
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 = 1; 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<<j)&1 
					2. 根据k的值来动态变化,使得枚举集合S-j中的所有点:
						2.1 获得S-j的集合: (S^(1<<j)):去掉集合S中的j点
						2.2 在S-j集合中,右移k位,获得第k位的值&1,即用k来遍历集合S-j中的1,这些1就是S-j中的点,这样就实现了枚举集合中 S-j的所有点
					*/
					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++)
		{
			double p = dp[(1 << n) - 1][i];//以0为出发点,经过所有顶点到达点i的最短距离		q
			cout << ans << endl;
			if (ans > p)
			{
				ans = p;
			}
		}
		printf("%.2lf", ans);
	}
}
posted @ 2023-02-08 17:04  hugeYlh  阅读(64)  评论(0编辑  收藏  举报  来源