题目大意
有一个“不寻常”的天平,天平上有C个挂钩,现在有G个物品,问有多少中挂法使得天平平衡(也就是力矩平衡)
分析
正如在之前动态规划专题中讲到的,动态规划的核心是对重复子问题的记录,很多问题都可以用搜索(枚举)来思考,而利用动态规划这一技巧则可以提高某些搜索问题的效率(顺便提一下,贪心、动规、分治都是十分重要且有效的技巧)。
最直接的枚举法有
2020
种情况
问题问的是有多少种方法,那么我们的目标函数(如果确定了是动态规划问题后就应该叫状态)就应该是和数目有关的。我们应该思考怎么表示状态,尝试一下能不能把问题缩小,在思考缩小的问题如何和原问题之间建立关系的过程其实就是思考状态的某个维度以及在这个维度下如何进行状态转移。
对状态的思考通常从问题的变量着手考虑,题目中的变量是和物品有关的,我们尝试在物品数量上进行变化。观察发现在我们已经放进去若干物品后再放入下一个物品的时候状态的改变之后此时两边的力矩差有关,这是一个很有用的信息,我们的状态就可以用和前i个物品有关的信息来表示,更进一步:
我们用
dp[i][k]
表示只放前i个物品的时候力矩差是j的挂法数,这只是一个初步的状态的设想,还需要看看能不能设计出对应于这个状态的转移方程,如果能够找到转移方程则说明我们的状态是对的,否则还需要修改状态。
状态转移的思考方式就类似于数学归纳法,假设我们已经知道了前i-1个物品的所有信息,在加入第i个物品的时候,我们有G个位置可以放,对于它放的每一个位置我们去更新挂法数。
容易得到转移方程:
i表示前i个物品,j表示放在第j个位置,k表示力矩差。
总结
这道题想了1个多小时,上面的分析是一边想一边写出来的。我觉得这是种很好的思维模式。
思维树:思考问题的过程就是对问题树进行搜索的过程,而我们在思考的过程中不单单是自上而下或自下而上的。我们可以把思维看成是一颗带权多叉树,边上的权值就是往这个方向思考正确的概率,在沿着这颗树走的过程中我们也不断接受反馈,从而对之前的权值进行更新,这个更新可能就意味着我们当前的思路不太行得通需要回溯回去重新思考。
我们的先验知识对树上的边权也有很大影响,对于这道题,如果我们一开始对01背包掌握的十分熟悉,那么我们想出这道题的概率也大大增加。
代码
#include<cstdio>
#include<iostream>
#include<cmath>
#include<cstdlib>
#include<cstring>
using namespace std;
int n,ans;
int dp[25][15005];
int C,G;
int c[25];
int g[25];
void Work()
{
memset(dp,0,sizeof(dp));
for(int j=1;j<=C;j++)//边界
dp[0][7500]=1;
for(int i=1;i<=G;i++)//前i个物品
for(int j=1;j<=C;j++)//放到第j个挂钩上
for(int k=0;k<=15000;k++)//力矩差是k的时候
{
if(k-c[j]*g[i]>=0 && k-c[j]*g[i]<=15000)
dp[i][k]+=dp[i-1][k-c[j]*g[i]];
}
cout<<dp[G][7500]<<endl;
}
int main()
{
while(scanf("%d%d",&C,&G)!=EOF)
{
for(int i=1;i<=C;i++)scanf("%d",&c[i]);
for(int i=1;i<=G;i++)scanf("%d",&g[i]);
Work();
}
}