动态规划:状态压缩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 @   hugeYlh  阅读(81)  评论(0编辑  收藏  举报  
编辑推荐:
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
· 基于Microsoft.Extensions.AI核心库实现RAG应用
· Linux系列:如何用heaptrack跟踪.NET程序的非托管内存泄露
· 开发者必知的日志记录最佳实践
阅读排行:
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· Manus的开源复刻OpenManus初探
· 写一个简单的SQL生成工具
· AI 智能体引爆开源社区「GitHub 热点速览」
· C#/.NET/.NET Core技术前沿周刊 | 第 29 期(2025年3.1-3.9)
点击右上角即可分享
微信分享提示