虽然现在人类已经很厉害,可以开发出ai,机器人。可是依旧还在探索自己的出处,那混沌世界初创时,我们从何而来。我们都知道 1+1=2. 但是真正只有少数开化的人才会去思考,为什么不是 1+1=10.  对于等于2是有它的道理,等于10(读作一零)也是对的。都表示1个再加一个是2个。都是对的真理。看,真理只有一个,表现形式却可以不同的,有多个表象。

很多人写算法,出手就是严密的思路,但是大量的周密反而让人不好理解。这也可能是反人类思维的。我们去做一件事,都是开始先试着做,不断修正错误;再到起码会做,然后再优化。其中修正错误伴随终身。这才是真正的学习之道吧。

背包问题,即通过动归找最优解。其实首先背包问题都可以通过穷举法,或者回溯法解决。这可能就是背包的创世纪吧。首先用最笨的办法,将问题解决方法搞清楚。再去优化。发现用动归,可以避免穷举时,没必要的一些值。即穷举时也要对树剪枝,做优化,省去明显不用考虑的组合。再就是保存一些中间结果,这就是动归。

背包问题其实是从两个维度考虑问题。

我们其实可以先从一个维度考虑。即物品只有重量。那么会衍生出,这个背包背的东西可以有多少种重量的问题。你会发现这时物品没有体积,没有价值等其他附加属性。

比如有3个物品A,B,C,重量分别是 2,3,6。那么它们的组合有7种(去掉了背包是空时的组合),因为每个物品只有选或者不选状态,即2种,那么3个物品就是 2*2*2=8,共有8种组合

穷举的算法复杂度是2。印度神庙的什么塔是264.要搬到世界末日了。 所以计算机算也是吃不消的。

另一种找出这7种可以用树或者回溯法:A, AB, ABC, AC,B, BC, C.

这种树形回溯,也可以看到当第一个元素是B时,没必要再去碰前面的元素A了。因为在 AB时,已经有AB的组合了。只要往后找元素就行。

可以做个练习,如果有 A,B,C, D四种元素的回溯顺序是什么。

将这些物品的所有组合,计算出组合出现的重量,就是所有可能出现的背包背的重量。

A=2; 

AB=2+3=5;

ABC=2+3+6=11;

AC=2+6=8;

... ...

你看到物品只有重量这个一维度的属性。如果我说背包只能承重7斤,你就可以找到它最多可以装到多少斤吧。

现在可以对上面的问题写算法了。用穷举或者回溯,加上剪枝;还有其他解法?

现在进入二维角度。我们说物品不光有重量,还有价值。比如A值5元,B值3元,C值4元。这时我们的解决不光要不能超重,还要价值最大。其实你发现我们还可以引申出物品还有空间,背包不是无限大的,那么这就要从3维角度考虑。

 

一维的问题用一维数组可以解决;二维的用二维数组;三维就需要用三维数组解决。当然经过优化,二维也可以用一维解决。

 

用一维解决时,为什么必须从后往前遍历数组。因为前面的是用当前物品加入的,已经变化的话,前面的值会被修改,这样等于前面这个物品已经被加入了。如果再往后遍历,如果又引用到这个值,其实还是这个物品,就会被引入2此,和我们01背包问题不相符。重复利用了。

 

Dynamic Programming Algorithm to Solve 0-1 Knapsack Problem | Algorithms, Blockchain and Cloud (helloacm.com)

拿leetcode的题做分析:

416. 分割等和子集 - 力扣(LeetCode)

这道题最终的核心是对于一系列值,看能否凑成给定值。

 

开始时循环错误的:

外层循环用 1到 limit,里面循环 依次拿数据取碰撞,这时发现拿数据碰撞,这个数据就是会多次被用。即成了完全背包问题。不是01背包了。

 

1.1 第一版

#include <iostream>  
#include <unordered_map>
#include <functional>
#include <vector>
#include <numeric>
using namespace std;

void p(const unordered_map<int,int> v) {
    cout << endl;
    for (auto vi : v)
        cout << " " << vi.first;
    cout << endl;
}

bool canPartition(vector<int>& nums) {
    int sum = std::accumulate(nums.begin(), nums.end(), 0);
    if (sum % 2 != 0) return false;
    sum = sum / 2;
    cout << "half is: " << sum << endl;
    unordered_map<int, int> map;
   
        for (int digit : nums) {
            if (map.empty()) {
                map[digit] = digit;
            }
            else {
                for (auto pair : map) {
                    auto key=pair.first + digit;
                    if(key<=sum)
                    map[key] = key;
                }
            }
            p(map);
        }
        
    
    return map[sum] == sum;
}


int main(int argc, char** argv) {
     vector a = { 3, 3, 3, 4, 5 };
    cout << "can part:" << canPartition(a);
}
View Code

 

输出:

can part:
3

3 6 9

3 6 9

3 6 9 7

3 6 9 7 8
1

将上述代码提交后,已通过 115/117。剩下两个测试没通过。

这个版本最主要就是这部分:

    unordered_map<int, int> map;

    for (int digit : nums) {
      if (map.empty()) {
        map[digit] = digit;
      } else {
        for (auto pair : map) {
          auto key = pair.first + digit;
          if (key <= sum)
            map[key] = key;
        }
      }
      p(map);
    }

    return map[sum] == sum;

思路是,对一系列nums值,按照递进的思想,看能生成哪些值。将生成的值,放在map中。每次取下一个值时,和map里面的值再做叠加,看生成了什么值,再放入map。

等于每个都会和已经生成好的再去碰撞,所以数组里这些值是无所谓先后的。

比如:sums={ 3, 3, 3, 4, 5 };

先取3,因为map里面开始是空,直接放入map=(3)。

再取第二个3,先和集合中其他元素碰撞,加入。3+3=6,map=(3, 6); 最后把自己加入,map=(3, 6)

再娶 3,map=(3,6,3+3, 6+3)=(3,6,9);自己加入,不变。

再娶 4,map=(3, 6,9,3+4,6+4,9+4)=(3,6,9,7,10,13);自己加入  (3,6,9,7,10,13,4)

.......

优化,将大于指定和的数省去,>9 就不要了。

如果看到9,就直接找到,退出吧。

map改成集合。

 

版本1.2

这时,由于只用了一个map,在条件合适时就插入。造成了刚插入的值,又和自己去运算,等于一个值用了多次。

出错由于:(100),下一个99,这时:(100,100+99)=(100,199)。这时已经改动了原来的map,造成有可能下一个元素读取出来是199,这时又和99加,产生除了 199+99=298,这种根本不对的结果。

比如用如下示例时会出错。

vector a = {
100,100,100,100,100,100,
100,100,100,100,99,97
}

能否凑成 598.

 

通过代码:

#include <iostream>  
#include <unordered_map>
#include <functional>
#include <vector>
#include <numeric>
using namespace std;

void p(const unordered_map<int,int> v) {
    cout << endl;
    for (auto vi : v)
        cout << " " << vi.first;
    cout << endl;
}

bool canPartition(vector<int>& nums) {
    int sum = std::accumulate(nums.begin(), nums.end(), 0);
    if (sum % 2 != 0) return false;
    sum = sum / 2;
    cout << "half is: " << sum << endl;
    unordered_map<int, int> map;
   
        for (int digit : nums) {
            unordered_map<int, int> temp;
            
            if (map.empty()) {
                map[digit] = digit;
            }
            else {
                for (auto pair : map) {
                    auto key=pair.first + digit;
                    if(key<=sum)
                        temp[key] = key;
                }

                map.insert(temp.begin(), temp.end());
            }
            p(map);
        }
        
    
    return map[sum] == sum;
}


int main(int argc, char** argv) {
    vector a = {
    100,100,100,100,100,100,
    100,100,100,100,99,97
    
    };
    cout << "can part:" << canPartition(a);
}
View Code

输出:

can part:half is: 598

 100

 100 200

 100 200 300

 100 200 300 400

 100 200 300 500 400

 100 200 300 500 400

 100 200 300 500 400

 100 200 300 500 400

 100 200 300 500 400

 100 200 300 500 400

 299 499 100 200 500 300 199 400 399

 299 499 100 200 500 300 199 400 399 597 396 397 596 197 497 297 296 496
0
View Code

 

版本1.3:

优化成用set。注意因为是用found赋值true判定找没找到,所以,一上来判断digit是不是sum是必须的。否则,这种情况就会没有判断二造成失败。

bool canPartition(vector<int>& nums) {
    int sum = std::accumulate(nums.begin(), nums.end(), 0);
    if (sum % 2 != 0) return false;
    sum = sum / 2;
    cout << "half is: " << sum << endl;
    set<int> map;

    bool found = false;
    for (int digit : nums) {
       
        if (digit == sum) {
            //done
            found = true;
            break;
        }

        if (map.empty()) {
            map.insert(digit);
        }
        else {
            set<int> temp;
            for (auto pair : map) {
                //
                auto key = pair + digit;
                if (key == sum) {
                    //done
                    found = true;
                    break;
                }
                if (key < sum) {
                    temp.insert( key);
                }
                    
            }
            
            //融合到主流循环的map。
            map.insert(temp.begin(), temp.end());

            //别忘了自己插入。而且必须这里最后插入,不能前面开始就插入,否则自己和自己运算了。
            map.insert(digit);

        }
        p(map);
        
        if (found)
            break;
    }


    return found;
}

从这里我们发现set,可以用数组代替。但是我们要绝对避免在轮询set时,将刚才插入的值轮询到。因为插入的值我们看到都是加起来的和,往后走的,所以肯定是往后面插入的,我们如果改用数组,就从后往前遍历,这样就避免了轮询到刚插入值的问题。同时也避免用temp临时set来不断合并的问题。

这里主要是明白了,改成set其实可以进一步成数组,另外就是为什么遍历数组要倒序!

只要一个数组就搞定了。

以前算法都是击败5%。数组后击败 98%了,哈哈

bool canPartition(vector<int>& nums) {
    int sum = std::accumulate(nums.begin(), nums.end(), 0);
    if (sum % 2 != 0) return false;
    sum = sum / 2;
    cout << "half is: " << sum << endl;
    vector<int> map(sum + 1, -1);

    bool found = false;
    for (int digit : nums) {

        if (digit == sum) {
            //done
            found = true;
            break;
        }

        if (digit > sum) continue;

        for (int i = sum; i > 0; i--) {
            if (map[i] > -1) {
                auto key = map[i] + digit;
                if (key == sum) {
                    //done
                    found = true;
                    break;
                }
                if (key <= sum) {
                    map[key] = key;
                }
            }
        }
        //别忘了自己插入,必现这里最后插入。不能在前面
        map[digit] = (digit);

        //p(map);

        if (found)
            break;
    }//nums

    return found;
}

再改成while语句会更好:

注意这里的 数组下标值含义也变了,存的值也变了。

bool canPartition(vector<int>& nums) {
    int sum = std::accumulate(nums.begin(), nums.end(), 0);
    if (sum % 2 != 0) return false;
    sum = sum / 2;
    cout << "half is: " << sum << endl;

    //这个数组下标表示能否求和成i这个值。map[i]==0表示目前还不行。非0就表示可以。
    vector<int> map(sum + 1, 0);

    auto it = nums.begin();
    while (it != nums.end() && map[sum] == 0) {
        cout << "number: " << *it<<endl;
        if (*it == sum)
            map[sum] = 1;

        //因为这里都是和已有元素取叠加,都超过sum了,就不要了。
        if (*it > sum) {
            it++;//这里别忘了移动到下一个元素
            continue;
        }
            

        auto i = sum;
        while (i-- > 0) {
            if (map[i] ==1 ) {//表示可以合成i
                auto key = i + *it; //改成 i+那个要处理的数字
                if (key <= sum) {
                    map[key] = 1;
                }
            }
        }
        //别忘了自己插入,必现这里最后插入。不能在前面
        map[*it] = 1;

        it++;
        p(map);
    }

    return  map[sum] != 0;
}

第一个元素可以直接插入数组,因为当前数组里面的有效值是没有的。

继续优化成数组是bool型的:

bool canPartition(vector<int>& nums) {
    int sum = std::accumulate(nums.begin(), nums.end(), 0);
    if (sum % 2 != 0) return false;
    sum = sum / 2;
    //cout << "half is: " << sum << endl;

    //这个数组下标表示能否求和成i这个值。map[i]==0表示目前还不行。非0就表示可以。
    vector<bool> map(sum + 1, false);

    auto it = nums.begin();

{
//直接把第一个元素放入
if(*it<=sum)
map[*it]=true;
it++;    
}


    while (it != nums.end() && !map[sum] ) {
        if (*it == sum)
            map[sum] = true;

        //因为这里都是和已有元素取叠加,都超过sum了,就不要了。
        if (*it > sum) {
            it++;//这里别忘了移动到下一个元素
            continue;
        }
            

        auto i = sum;
        while (i--) {
            if (map[i] ) {//表示可以合成i
                auto key = i + *it; //改成 i+那个要处理的数字
                if (key <= sum) {
                    map[key] = true;
                }
            }
        }
        //别忘了自己插入,必现这里最后插入。不能在前面
        if(! map[*it])
           map[*it] = true;

        it++;
    }

    return  map[sum];
}
View Code

 

用二维数组,列优先实现

bool canPartition(vector<int>& nums) {
    int sum = std::accumulate(nums.begin(), nums.end(), 0);
    if (sum % 2 != 0) return false;
    sum = sum / 2;
    //cout << "half is: " << sum << endl;

    //这个数组下标表示能否求和成i这个值。map[i]==0表示目前还不行。非0就表示可以。
    //用了二维数组,第0列没用。添加了第0行,为了动规时找上一个状态。
    //动规是按照 列优先 计算的。先算完第一列,再算第二列....
    //01背包时,列优先,还是行优先都是可以的,在用二维数组的时候。
    //但是一维数组必须用行优先,且需降序实现。因为如果递增的话,前面的最优值,已经是
    //选用过了当前的数字,如果再去用前面的值,会造成重复用,违反了01背包,只能用一次原则。
    vector<vector<bool>> map(nums.size()+1,vector<bool>(sum + 1, false));

    //初始化,目标是0,肯定不能都能加到
    for (int i = 0; i <= sum; i++) {
        map[0][i] = false;
    }

    //目标从1,一直到要计算的sum
    for (int i = 1; i <= sum; i++) {
        //j是当前行的控制;但是去nums数组其实是从0开始的,所有jj用j-1来实现取数组值。
        //因为插入了一行初始行,由于代码种要取上一次状态:map[j - 1][i]
        for (int j = 1;j <=nums.size(); j++) {
            int jj = j - 1;
            
            if (i == nums[jj]) map[j][i] = true;//和目标i相等,肯定可以加到。
            else {
                //如果以前就能加到目标,再加入个数字,都不用用,那肯定也能加到;
                //否则需要目标i要比现在加入的数字大,且去掉这个数字后,也能加到。
                if (map[j - 1][i]||( i> nums[jj]&&map[j - 1][i - nums[jj]] ) ) 
                    map[j][i] = true;
            }
        }
    }
    return  map[nums.size()][sum];
}

一维数组的:

bool canPartition(vector<int>& nums) {
    int sum = std::accumulate(nums.begin(), nums.end(), 0);
    if (sum % 2 != 0) return false;
    sum = sum / 2;

    //一维数组。optimal[1]存储和是1;optimal[sum]存储和是sum的。所以需要分配sum+1个元素。optimal[0]没用
    vector<bool> optimal(sum + 1, false);

    //不能这样初始化,引起单个某个值多次引用。这是01背包,不可以。
    //for (int i :nums) {
    //    if (i <= sum)
    //        optimal[i] = true;
    //    
    //}

     for (int i:nums) {//iterate number
        if (i > sum) continue;        
        for (int j = sum;j >=1; j--) {// iterate from 1 to sum
            if (optimal[j]) continue;
            //值和要加的和一样,或者 要形成的和大于这个值,看前面能形成不。
            if (i == j || (j > i && optimal[j - i])) optimal[j] = true;
        }
    }
    return  optimal[sum];
}

优化剪枝了一维数组:求的和,遍历到nums里面元素最小值就行了。比如要求的和是5,nums里面的11是不能加法形成5的。

bool canPartition(vector<int>& nums) {
    int sum =nums[0], start=nums[0];
    for (int i=1;i<nums.size();i++) {
        sum += nums[i];
        if (nums[i] < start) start = nums[i];
    }
    if (sum % 2 != 0) return false;
    sum = sum / 2;
    //cout << "half is: " << sum << endl;

    vector<bool> map(sum + 1, false);

    for (int i:nums) {//iterate number
        if (i > sum) continue;        
        for (int j = sum;j >=start; j--) {// iterate sum
            if (map[j]) continue;
            if (i == j || (j > i && map[j - i])) map[j] = true;
        }
    }
    return  map[sum];
}