subset sum made simple论文精讲

摘要:来自论文"sub set sum make simple"。本实验针对“subset sum”问题,复现了”subset sum  make simple”论文的算法,并且提出了自己的算法(慢的不可描述),并且在自己制作的数据集上进行了检测,发现人家的东西就是很快,针对我的动态规划算法居然不如别人的分治算法这件事进行了深入探索,并且调整了作者的算法(谈不上大改动)

·问题描述

给你一个集合S,一个数字u,问你能不能从S中找出几个数,是这几个数的和是u。

·算法思路

1.先讲讲人家作者是怎么干的。

红线上面这个函数,先别管他, 且待下文分解。

        能看出个什么呢?首先肯定是分治算法S,Q,R这些东西都是集合,集合也就能进行个赋值、排序之类的操作,显然重点就是红线上面的函数干了什么事情。这个东西的传参,先传进来了Ql这个东西,这是个集合啊。这个集合,往根本上靠,是来自Sl这个集合的。Sl是模b同余这件事情的聚类,也就是说Sl中的每一个元素x,都有以下事情成立:

      存在k, x=k*b+l

        接下来就好说了,Ql自然就是这些k的集合,这不就分出堆来了嘛。分治怎么分的,自然是根据÷b得到的余数进行分类,余数相同的分为一类(针对S集合),然后只研究除数(针对Q集合)(显然,一个类里面除数不能也相同,烦请读者自证之)

        R那里里,z*b+l*j,既然我知道b是除数,l是余数,Q又是那些商,我自然理所当然的认为R是把之前那些商(Q集合里装的东西)变回了给定集合S里面的那些元素。(事实上,j=1,使得l*j就是余数)。

       我们现在知道了什么呢?算法自己定义了一个b,并且以这个b作为除数,通过余数相同还是不相同划分了S中的元素,从而实现了分治。这是如何“分”的问题,但是如何“治”,正隐藏在画红线的函数中。

2.红线函数之谜

        以上就是红线上面的函数。聪明的你一定已经看出来了,这个小函数里同样进行了分治算法。如果传入集合(S)里只有一个元素,就返回两个二元组(第一个return);如果不是,就一直二分拆分,直到全都拆成只有一个元素(第二个return)。最后根据定义,把所有的二元组按照规矩生拼在一起。

        根据分治的思想,我希望在每一个小类里面(小类都经过了这个带#的函数的处理)都找到符合要求的(其实现在就是说元素全加起来别比上界u还大)的这些个集合(的每个元素之和),事实上,当前只是在小类里面,你比上界小,在R里面跟别的小类融合融合说不定还有可能达到u;现在就比u还大了,这等大逆不道可还了得!必须进行剪枝!

         最后,别忘了分治和动态规划是一家人,我要说点关于01背包的东西了。就是说为什么(0,0)这个元素必须进来,01背包里面就有“装的下,但我就是不装”这件事,在这里同样有。小类里面组合的时候,自然包括要和不要,这个(0,0)就是不要的意思。

        最后说一句,主要是怕你已经忘了前面1中的结论了。这里二元组(x,y)中,x就是商,y就是余数的个数((x,1)中的1就代表一个余数),而且这里大家余数(不是个数)都一样,除数不相同但被除数相同,写一个除法公式出来看看,自然就知道这里说的是什么了。

3.知识的涌现

前面写的云里雾里,到这里终于可以统一的说一说了。

       按照mod b的余数进行了类的划分,实现了第一次分治;在每一个小类里面,分解成单个的元素,再进行组合,这是第二次分治,同时去掉了小类里面组合出来的大于u(上界)的情况,算是第一次剪枝。然后,R里面统一了所有可能正确的小类,并且是将“将商当成代理人,进行S#函数运算”的元素通过zb+lj恢复成了S中元素本来的样子,最后返回的的是小类里面组合(加和)起来不如u大的所有情况,自己找一个真正等于u的不难。找不到,就是组合不出来。这里,进行了第二次剪枝,剪掉了比u大的枝条。

4.复杂性分析

讲讲最复杂的,作者的方案为什么快:

定义几样东西

接下来,有几条引理:first:

(A)别问我为什么是对的,我不懂FFT(傅里叶变换),就是对的(但是为什么是u是个小问题)

(B)既然(A)是对的,自然有k个集合的时候(B)就是对的

(C)既然问题空间变成了u*v这么大(排列组合我还是会的),自然根据A,C也没得说

Second:

作者的想法很巧妙。直接硬来肯定是不行。但是,我把s#拆成相同大小的两部分,没问题吧,

肯定是对的。根据上面的C引理,我拼一拼,把常数都去掉,真就让作者碰对了。

Thrid:

千万别看论文的证明:简单的东西变得极端复杂。我们已知了B的情况,实际上,S和S#时间复杂度相同。这里,分的比较均匀的情况下,S中元素数量就是n,但是再mod过程中x按比例缩小了b,u自然也按比例减小b。至于为什么b没出现在对数符号里,就是因为这一项太小了,时间复杂度中不予考虑(难道还能出一个数量级吗)?

Total:总共:计算发现,计算这几个子集是时间复杂度的主体,带入C,就是这个样子。不信可以自己算一算,R处的合并占用时间不多。

·比较算法

终于可以讲一件我的算法问题是什么了。我的方法是递归,中途进行剪枝。

经过仔细地检验,这个算法甚至能到达2^n的复杂度,问题就是出在答案引理一那里。同样的运算,我的时间复杂度很可能变成2^n,但是作者的傅里叶变换方法不会。强就强在我不知道如何证明的那条引理上了。

最后,给出复现成果:

#include <iostream>
#include <vector>
#include <utility>
#include <algorithm>
#include <cmath>
#include<time.h>
using namespace std;
// Shift+Alt+F

// ComputingPairwiseSums_2D
vector<pair<int, int>> CPS2(vector<pair<int, int>> A, vector<pair<int, int>> B, int u);
// ComputingPairwiseSums
vector<int> CPS(vector<int> A, vector<int> B, int u);
// AllSubsetSums_2D
vector<pair<int, int>> AllSubsetSums_2D(vector<int> S, int u);
// AllSubsetSums
vector<int> AllSubsetSums(vector<int> S, int u);

int main()
{
    clock_t start, end;
    int n;         // size of S
    vector<int> S; // set S
    int u;         // upper bound u
    cout << "请传入集合中元素数量" << endl;
    cin >> n;
    cout << "请传入元素" << endl;
    while (n > 0)
    {
        int temp;
        cin >> temp;
        S.push_back(temp);
        n--;
    }
    cout << "请传入上界" << endl;
    cin >> u;
    start = clock();
    vector<int> ret1 = AllSubsetSums(S, u);//有序的
    end = clock();
    cout << "the time cost is " << end - start << endl;
    double temp = ret1.back();
    if (temp == u)
        cout << "true" << endl;
    else
        cout << "false" << endl;
    return 0;
}

vector<pair<int, int>> AllSubsetSums_2D(vector<int> S, int u)
{
    int size = S.size();
    if (size == 1) // S={x}
    {
        vector<pair<int, int>> ret{ make_pair(0, 0), make_pair(S[0], 1) };//(0,0)相当于不要,后者是要(1保存余数),S[0]是除法的最终结果
        return ret; // return{(0, 0), (x, 1)}
    }
    vector<int>::const_iterator first1 = S.begin();
    vector<int>::const_iterator last1 = S.begin() + (size / 2);
    vector<int>::const_iterator first2 = S.begin() + (size / 2);
    vector<int>::const_iterator last2 = S.end();
    vector<int> T(first1, last1); // T = an arbitrary subset of S of size n/2
    vector<int> L(first2, last2); // L = another arbitrary subset of S of size n/2
    return CPS2(AllSubsetSums_2D(T, u), AllSubsetSums_2D(L, u), u);
}

vector<pair<int, int>> CPS2(vector<pair<int, int>> A, vector<pair<int, int>> B, int u)
{
    vector<pair<int, int>> ret;
    int sizeA = A.size(), sizeB = B.size();
    for (int i = 0; i < sizeA; i++)
    {
        for (int j = 0; j < sizeB; j++)//遍历
        {
            int fir = A[i].first + B[j].first;   // first num of the pair
            int sec = A[i].second + B[j].second; // second num of the pair
            if (fir <= u && sec <= u)
            {
                pair<int, int> p(fir, sec);
                ret.push_back(p);//弄一块
            }
        }
    }
    sort(ret.begin(), ret.end());
    vector<pair<int, int>>::iterator pos = unique(ret.begin(), ret.end());
    ret.erase(pos, ret.end());
    return ret;
}

vector<int> AllSubsetSums(vector<int> S, int u)
{
    int n = S.size();
    int b = sqrt(n * log2(n));//定义了一个奇怪的东西,为什么是2?分的均乎?
    vector<int> ret;
    vector<vector<int>> R;
    for (int i = 0; i < b; i++)//分成b个类
    {
        vector<int> Si, Qi, Ri;
        // get Si
        for (int j = 0; j < S.size(); j++)
        {
            if (S[j] % b == i)//分类器
                Si.push_back(S[j]);
        }

        // get Qi
        for (int j = 0; j < Si.size(); j++)
        {
            Qi.push_back((Si[j] - i) / b);//k
        }

        // get SQi
        vector<pair<int, int>> SQi = AllSubsetSums_2D(Qi, u / b);//k的上界

        // get Ri
        for (int j = 0; j < SQi.size(); j++)
        {
            Ri.push_back(SQi[j].first * b + i * SQi[j].second);
        }
        sort(Ri.begin(), Ri.end());
        //排列去重
        vector<int>::iterator pos = unique(Ri.begin(), Ri.end());
        Ri.erase(pos, Ri.end());
        R.push_back(Ri);
    }
    ret = R[0];
    for (int i = 1; i < R.size(); i++)
    {
        vector<int> temp = R[i];
        ret = CPS(ret, temp, u);
    }
    return ret;
}

vector<int> CPS(vector<int> A, vector<int> B, int u)
{
    vector<int> ret;
    int sizeA = A.size(), sizeB = B.size();
    for (int i = 0; i < sizeA; i++)
    {
        for (int j = 0; j < sizeB; j++)
        {
            int p = A[i] + B[j];
            if (p <= u)
                ret.push_back(p);
        }
    }
    sort(ret.begin(), ret.end());
    vector<int>::iterator pos = unique(ret.begin(), ret.end());
    ret.erase(pos, ret.end());
    return ret;
}

这里声明一下,time cost 单位不是秒,是时钟周期。

总结:

从实验本身讲,作者的成功无疑是建立在FFT(快速傅里叶变换)上的。这个算法将u^2级别的复杂度降低到了ulog(u)。

最后加入一些分治方法进行剪枝,使得算法的时间复杂度很低。

如果说还有一些问题,一定是剪枝不够彻底,显然拆分出子问题后,规模上讲,子问题规模的上界相对于子问题来说,和拆分之前比较,显然是放宽了。

跳出问题将一些方法论的话,直接看伪代码并不容易看懂,但是实现成代码后再看代码轻松了一些,似乎是逻辑串接上了。直接看作者的引理并不容易,我的方法是根据时间复杂度之间的公式上的关系,总结规律自行思考,再和作者对思路。有时候直接吃别人嚼过的馒头反而嚼不动,有时候自己重新做一遍,能找到别人使用了但是没写在纸面上的东西。

附录:

我说了,快速傅里叶变换我没看懂。这里是一些个人的小小见解,不保证正确,所以写成了绿色英文。有兴趣可以看看,交流一下思想。

Sublimation at the end of the article:The reason why I use English is that:only if you’re very interested in it, you will read about the content over there.If you read it, you’re my friend in study.

I’m not very confident about the thing I’m going to say, so if you have different opinion, please talk with me.

So why the lemma1 -> (A) is true? Mabe I can prove it without FFT. Define a set x, with Num(x) is the number of elements of x. It is obvious that Num(S)<u, while Num(T)<u.

Sort S and T, then I can chose element t from set T in order, while use the binary lookup to find the element s in S,which is the biggest element of S that satisfys “t*s<u”,at the time cost of

log(Num(s)).Because I chose every t of T, I do it for Num(T)times, which time complexity is Num(T)*log(Num(S)),and the author may approximate it as u*log(u).Mabe this is the core of FFT.

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值