我在互联网上做一些算法练习时发现了拔河问题:
声明:
给定一组n个整数,将该组分成两个n / 2个大小的子集,每个子集的两个子集之和尽可能小。如果n是偶数,则两个子集的大小必须严格为n / 2,如果n为奇数,则一个子集的大小必须为(n-1)/ 2,其他子集的大小必须为(n + 1)/ 2
例如,让set set为
{3, 4, 5, -3, 100, 1, 89, 54, 23, 20}
set的大小为10.此set的输出应为
{4, 100, 1, 23, 20}
和
{3, 5, -3, 89, 54}
两个输出子集的大小均为5,两个子集中的元素总和相同(148和148)。
让我们考虑另一个例子,其中n是奇数。让给定的集合
{23, 45, -34, 12, 0, 98, -99, 4, 189, -1, 4}
输出子集应为
{45, -34, 12, 98, -1}
和
{23, 0, -99, 4, 189, 4}
两个子集中的元素总和分别为120和121.
我在互联网上找到了一种背包式的方法,同时寻找解决方案here:
我没有得到解决方案的这一部分:
for (int i = 1; i <= N; ++i)
for (int j = sum; j >= 0; --j)
if (dp[j])
dp[j + W[i]] |= dp[j] << 1;
我知道我们正试图找到占i总和的对象数量。但是dp[i] << 1
正在做的事情我没有得到正确的解决方法:
另外,为什么我们向后迭代j变量,从sum开始到0?为什么不总和0?
有人可以用更简单,更通用的方式告诉我基础逻辑吗?
由于
答案 0 :(得分:2)
我认为评论很清楚,实际上。
// If (dp[i] << j) & 1 is 1, that means it is possible
// to select j out of the N people so that the sum of
// their weight is i.
从初始条件开始dp [0] = 1&lt;&lt; 0,意思是&#34;它可以选择0个人,使得他们的权重之和为0&#34;。
然后,对于dp中非零的每个条目(即if (dp[j])
部分),您将更新列表中当前人员的dp。
现在,您特别询问&#34;为什么dp[j] << 1
&#34;?好吧,想象前3个元素是1,2,3。
然后dp [3]将是二进制的110,意思是&#34;你可以使用1个人(3)或两个人(1和2)得到3的总和。
如果下一个数字是10,那么当我们来到dp [3]时,我们做dp[3+10] |= dp[3] << 1
,得到dp [13] 1100.含义&#34;因为我们可以用2个人或一个人,在混合中额外增加10人,我们可以与3人(1,2,10)或2人(3,10)和#34进行13次
然后在结束时,您需要做的就是在dp中查找最接近总和的一半的条目。当然,其他团队的总和将是总和减去这个值。
请注意,此算法不会告诉您需要选择哪些数字来获得该总和;这些信息丢失了。它只会告诉你最好的两个总和是什么。虽然稍微修改算法并使用一些数据结构来保留该信息并不困难(这样dp中的每个条目都表示&#34;我可以从3个数字中得到这个总和,并且这些数字是...... &#34;。)
哦,关于向后的迭代:这是为了防止我们两次计算相同的数字。如果第一个条目是1,我们说&#34;我可以从0个数字中取0;现在我可以从1号和#34;中取1。然后立即,&#34;我可以从1号码中取1;现在我可以从2个数字和#34;中取2。等等。
编辑:既然你问过,这里有一种方法可以做到这一点(请注意,如果你输入非正数,它将会中断,我会留给你解决这个问题):
int N;
int W[100 + 5];
std::map<int, std::vector<int>> dp[450 * 100 + 5];
void solve()
{
int sum = accumulate(W + 1, W + N + 1, 0);
// If (dp[i][j]) contains a nonempty vector, that means it is possible
// to select j out of the N people so that the sum of
// their weight is i, with those people's indices being the values of said vector
dp[W[1]][1].push_back(1);
for (int i = 2; i <= N; ++i)
{
for (int j = sum; j >= 0; --j)
{
for (std::map<int, std::vector<int>>::iterator it = dp[j].begin(); it != dp[j].end(); ++it)
{
dp[j + W[i]][it->first+1] = it->second;
dp[j + W[i]][it->first+1].push_back(i);
}
}
}
std::vector<int> selected;
int minDiff = 450 * 100;
int teamOneWeight = 0, teamTwoWeight = 0;
for (int i = 0; i <= sum; ++i)
{
if (!dp[i].empty())
{
int diff = abs(i - (sum - i));
if (diff < minDiff)
{
minDiff = diff;
teamOneWeight = i;
teamTwoWeight = sum-i;
selected = dp[i].begin()->second;
}
}
}
cout << "Team 1, sum of " << teamOneWeight << ": ";
for (int i = 1; i <= N; ++i)
{
if (std::find(selected.begin(), selected.end(), i) != selected.end())
cout << W[i] << ' ';
}
cout << endl << "Team 1, sum of " << teamTwoWeight << ": ";
for (int i = 1; i <= N; ++i)
{
if (std::find(selected.begin(), selected.end(), i) == selected.end())
cout << W[i] << ' ';
}
cout << endl;
}
答案 1 :(得分:1)
这种方法可以被认为是使用动态编程来计算三维数组f的内容。
f定义为:
f[i][sum][j] = 1
如果可以使用范围为1..i的j的权重来产生总和的权重。
现在假设我们知道f[i][sum][j]
对于i,sum,j的某些值是1。如果我们决定包括权重i,那么我们推断出f[i+1][sum+W[i]][j+1]
也必须是真的。
f[i+1][sum+W[i]][j+1] |= f[i][sum][j]
如果我们不包括权重i,那么我们推断f[i+1][sum][j]
也必须是真的。
第一个优化是将第三维存储在单个64位整数中,而不是64位1位整数中。这使代码运行得更快。
假设f [i] [sum] =二进制1001,这意味着对于j等于0或3,f [i] [sum] [j]为1.我们现在得出结论,我们需要设置f [i] [sum + w [i]] [j]等于1,j等于1或4,因此我们需要OR与二进制10010 = 1001 <&lt; 1,这解释了&lt;&lt; 1操作。
第二个优化是发现我们可以为i的所有值重用相同的数组,但是要做到这一点我们需要向后迭代,否则我们会认为我们可以多次使用第i个元素。