拔河比赛:将n个对象的集合划分为子集

时间:2014-10-29 19:19:30

标签: c++ c algorithm bit-manipulation dynamic-programming

我在互联网上做一些算法练习时发现了拔河问题:

声明

给定一组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?

有人可以用更简单,更通用的方式告诉我基础逻辑吗?

由于

2 个答案:

答案 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]也必须是真的。

优化1

第一个优化是将第三维存储在单个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操作。

优化2

第二个优化是发现我们可以为i的所有值重用相同的数组,但是要做到这一点我们需要向后迭代,否则我们会认为我们可以多次使用第i个元素。