C++ 算法系列之贪婪算法

贪婪算法的本质

活动选择问题。

假设有n个活动,这些活动有起止时间,这些活动都要使用同一个如教室这样的资源。每次只能有一个活动使用该资源。我们定义两个活动起止时间不相交,则称这两个活动是相容的。求一个最大相容活动。
这个问题如果用暴力解法,则对于n个活动,复杂度为2的n次方.
我们采取的策略是始终选取结束时间最早的活动作为我们的解集合成员。这个策略就是贪婪选择的结果。
那么解释一下,为什么选择结束时间最早的呢?

定义Si 为i 活动的开始时间, Fi为i活动的结束时间
定义Ak为第k个活动
定义S(i,j) = {Ak 属于S:Fi<=Sk<=Fk<=Fj}, S(i,j)是由所有与Ai和Aj相容的活动构成。
Am是S(i,j)中最早完成的一个活动。Fm = min{Fk:Ak属于S(i,j)}
设A(i,j)是S(i,j)的一个最大相容活动子集,Ax 是A(i,j)的第一个活动,如果Ax!=Am, 构造子集B(i,j) = A(i,j)-{Ax} U {Am}。由于Fm <= Fx, 所以B(i,j)中的活动是互不相交的。B(i,j) 和A(i,j) 的数目是一样的。因此B(i,j)是S(i,j)的一个包含Am的最大相容活动子集。
既然Am是最大相容子集中的一个元素。因此我们可以直接把Am作为我们解的集合中的一员。
接着证明S(i,m)为空。这里采用反证法,假设S(i,m)不空,则有活动Ay有 Fi<=Sy<Fy<=Sm<=Fm 。也就是Ay比Am更早结束。此时与Am最早结束矛盾。所以S(i,m)是空。
综合这两点,我们得到,最早结束的活动是最大相容活动子集的成员,并且最早结束的活动是最大相容活动子集的第一个成员。因此我们把最早结束的活动放到我们的解中。

OK, 既然已经证明了最早结束的活动是在问题的解中,那么我们只要不停的抽取最早结束的活动就好了。为了方便,我们在S(i,j) 左右两侧各插入一个活动,作为起始活动和终止活动,起始活动的开始和结束时间记成0,终止活动的开始和结束时间记成最大的整数

int * recursive(int *s, int *f, int i, int j)//输入参数,所有活动的起始时间和结束时间,以及活动的起始序号和终止序号
{
    static int *x = new int[j];//总共j个活动, 默认为0,活动满足需要则将该值标明为1
    int m = i + 1;//从第i个活动之后的活动开始
    while (m < j && s[m] < f[i])//寻找第一个在第i个活动结束时间之后出现的活动
    {

        x[m++] = 0;//起始时间不在第i个活动结束时间之后的,都记做0,意味着不选择

    }
    if (m < j)//首先判断是否序号是否在起始序号和结束序号之间
    {
        x[m] = 1;//找到起始时间在第i个活动之后的最早的活动后,将该活动记1,标明选择了该活动
        recursive(s, f, m, j);//递归,将起止活动调整成新选中的活动
    }
    return x;
}
int main()
{
    int *x;
    int s[] = { 0,1,3,0,5,3,5,6,8,8,2,12,numeric_limits<int>::max() };//活动列表0活动和max活动的起始时间
    int f[] = {0,4,5,6,7,8,9,10,11,12,13,14,numeric_limits<int>::max()};//活动列表0活动和max活动的终止时间
    x = recursive(s, f, 0, 12);
    for (int i = 1; i < 12; i++)
    {
        cout << x[i] << " ";
    }
    cout << endl;
    delete[]x;

}

背包问题

0-1背包问题
这种问题很简单,也最好理解,很容易得到其递推表达式
F[i,j] = max(F[i-1,j],F[i-1,j-Wi]+Vi
初始值F[i,j] 由w[j]和j的大小决定,如果w[j]>j则为v[j],否则为0
二维数组很容易得到解

void knapssack(int *W, int *V, int N, int C)
{
    int **f = new int*[N], i, j, *value = new int[N] {0};
    for (i = 0; i<N; i++)
    {
        f[i] = new int[C+1] {0};
    }
    for (i = 0; i<N; i++)
    {
        f[i][0] = 0;
    }
    for (j = 1; j<=C; j++)
    {
        f[0][j] = j>=W[0] ? V[0] : 0;
    }
    for (i = 1; i<N; i++)
    {
        for (j = 1; j<=C; j++)
        {
            if (j>=W[i])
            {
                f[i][j] = f[i - 1][j]>(f[i - 1][j - W[i]] + V[i]) ?
                    f[i - 1][j] : (f[i - 1][j - W[i]] + V[i]);
            }
            else
            {
                f[i][j] = f[i - 1][j];
            }
        }
    }
    i = N - 1;
    j = C;
    while (i>0)
    {
        if (f[i][j] == f[i - 1][j - W[i]] + V[i])
        {
            value[i] = 1;
            j = j - W[i];
        }
        i--;
    }
    if (f[i][j])
    {
        value[i] = 1;
    }
    for (i = 0; i < N; i++)
    {
        cout << value[i] << " ";
    }
    cout << endl;
    delete[]value;
    for (i = 0; i < N; i++)
    {
        delete f[i];
    }
    delete[]f;

}

这道题也可以用一维数组来求解,不过一维数组就只能给出最大值,却不能获得这个最大值是由哪些物品组成的,

void knapssack(int *W, int *V, int N, int C)
{
    int *f = new int[C+1]{ 0 }, i, j;
    stack<int> s;
    for (i = 0; i<N; i++)
    {
        for (j = C; j>0; j--)//注意这里必须是逆序输出。解释一下原因。比如, 我们让j按照1到C的顺序进行递增。我们现在开始计算i=2的时候f数组里面的元素值,首先我们知道此时f数组存放了i=1时计算过的数据。假设f[1]=0, W[2]=1,先对j=1的时候对f[1]进行计算,此时f[1]里面存放的就是i=2时的新数据了。在j=2的时候,W[2]=1,因此j>=W[2],f[j-W[i]]=f[2-1]=f[1],注意此时f[1]的值是i=2的时候算出来的,不是i=1的时候保留的旧值。所以这就和我们期望的f[i][j] = max(f[i-1][j],f[i-1][j-W[i]+Vi)不符合了。所以必须要从C到0递减,因为f[j-W[i]]肯定是在f[j]之后计算,这就保证了我们计算f[j]时用到的f[j-W[i]]的值一定是上一轮保留下来。
        {
            if (j>=W[i])
            {
                f[j] = f[j]>(f[j - W[i]] + V[i]) ?
                    f[j] : (f[j - W[i]] + V[i]);

            }
        }
    }
    cout << "max value is: " << f[C] << endl;
    delete[]f;
}

完全背包问题
和0-1背包的区别在于,0-1背包对于第i个物体只能选一次。而这里可以选择很多次
递推式是
F[i,j]=max(f[i-1,j], f[i,j-Wi]+Vi)
注意和0-1背包的递推式区别在于后面的比较对象。从理论上来说, 0-1背包和完全背包的递推方式都是最后被放入背包的物体是否是第i个物体。 0-1 背包中,由于物体最多只能选择1次,因此,最后一个物体如果是i号物体,那么它一定不能在之前被放入背包,所以它的递推规则分为不选第i号物体进入背包和选择第i号物体进入背包。
完全背包不限制次数,所以它的递推公式是基于我挑的所有物体都不包括第i号物体和我挑包括i号物体在内的所有物体两种情况。挑包括i号物体在内的所有物体进入背包时,我不管之前有没有挑过i号物体,我保证我挑i号物体的次数中一定要留一次放到最后一次挑物体时进行。
二维数组的解法

void knapssack(int *W, int *V, int N, int C)
{
    int **f = new int*[N], i, j, *value = new int[N] {0};
    for (i = 0; i<N; i++)
    {
        f[i] = new int[C + 1]{ 0 };
    }
    for (i = 0; i<N; i++)
    {
        f[i][0] = 0;
    }
    for (j = 1; j <= C; j++)
    {
        f[0][j] = j >= W[0] ? V[0]*j/W[0] : 0;//唯一区别在于初始值变化了,因为由碎片的存在,所以用平均个数乘以单价价值从而得到总价值
    }
    for (i = 1; i<N; i++)
    {
        for (j = 1; j <= C; j++)
        {
            if (j >= W[i])
            {
                f[i][j] = f[i - 1][j]>(f[i][j - W[i]] + V[i]) ?
                    f[i - 1][j] : (f[i][j - W[i]] + V[i]);
            }
            else
            {
                f[i][j] = f[i - 1][j];
            }
        }
    }
    i = N - 1;
    j = C;
    while (i>0)
    {
        if (f[i][j] == f[i - 1][j - W[i]] + V[i])
        {
            value[i] = 1;
            j = j - W[i];
        }
        i--;
    }
    if (f[i][j])
    {
        value[i] = 1;
    }
    for (i = 0; i < N; i++)
    {
        cout << value[i] << " ";
    }
    cout << endl;
    delete[]value;
    for (i = 0; i < N; i++)
    {
        delete f[i];
    }
    delete[]f;

}

一维数组版的更容易了,和0-1背包只有一个顺序的差别

void knapssack(int *W, int *V, int N, int C)
{
    int *f = new int[C+1]{ 0 }, i, j;
    stack<int> s;
    for (i = 0; i<N; i++)
    {
        for (j = 1; j<=C; j++)
        {
            if (j>=W[i])
            {
                f[j] = f[j]>(f[j - W[i]] + V[i]) ?
                    f[j] : (f[j - W[i]] + V[i]);

            }
        }
    }
    cout << "max value is: " << f[C] << endl;
    delete[]f;
}

多重背包问题
这类就是加了一个限定,限定每个物品不能超过多少件这样的。
F[i,j] = max(F[i-1,j],F[i, j-Wi*k]+Vi*k这里需要添加一个k来限定,为啥,很容易,和完全背包相比,我们需要考虑第i种物品的量,事实上,就是我们将问题拆成了将i-1个物体放入j容量的背包和将i个物体放入j 容量的背包,只是,我们需要控制一下第i个物体的数量。
二维数组

void knapssack(int *W, int *V, int *Num, int N, int C)
{
    int **f = new int*[N], i, j, *value = new int[N] {0}, cnt;
    for (i = 0; i<N; i++)
    {
        f[i] = new int[C + 1]{ 0 };
    }
    for (i = 0; i<N; i++)
    {
        f[i][0] = 0;
    }
    for (j = 1; j <= C; j++)
    {
        cnt = min(j / W[0], Num[0]);
        f[0][j] = j >= W[0] ? V[0] * cnt : 0;
    }
    for (i = 1; i<N; i++)
    {
        for (j = 1; j <= C; j++)
        {
            if (j >= W[i])
            {
                cnt = min(j / W[i], Num[i]);
                for (int k = 1; k <= cnt; k++)
                {
                    f[i][j] = f[i - 1][j]>(f[i - 1][j - k*W[i]] + k*V[i]) ?
                        f[i - 1][j] : (f[i - 1][j - k*W[i]] + k*V[i]);
                }
            }
            else
            {
                f[i][j] = f[i - 1][j];
            }
        }
    }
    i = N - 1;
    j = C;
    while (i>0)
    {
        cnt = min(Num[i], j / Num[i]);
        for (int k = cnt; k >= 1; k--)
        {
            if (f[i][j] == f[i - 1][j - k*W[i]] + k*V[i])
            {
                value[i] = i;
                j = j - k* W[i];
                break;
            }
        }
        i--;
    }
    if (f[i][j])
    {
        value[i] = min(j/W[i],Num[i]);
    }
    for (i = 0; i < N; i++)
    {
        cout << value[i] << " ";
    }
    cout << endl;
    delete[]value;
    for (i = 0; i < N; i++)
    {
        delete f[i];
    }
    delete[]f;

}

部分背包问题
这种最简单,思路就是贪心策略,每次挑价值最大的即可

霍夫曼编码
霍夫曼编码,将字符用二进制进行编码字符,这些字符会以前缀码的形式进行编码。前缀码的特征是没有任何编码字是另一个编码字的前缀。主要是针对一个字母表,每个字母的频次是不一样的,现在用一颗二叉树去合并这些字母,每次都挑选最小频次的字母进行合并,最终合并成一颗满二叉树,使得字符在树中的深度和字符的频次的乘积的总和最小。
霍夫曼编码构建的树中只有度为0和2的结点,不存在度为1的结点。度指的是某一节点后有几个分叉。

class BinaryTree
{
private:
    int key;
    char data;
    BinaryTree*left;
    BinaryTree *right;
public:
    friend BinaryTree* huffman(int *f, char *d, int n);
    friend void printCode(BinaryTree *, string c);
    BinaryTree(int k, char d, BinaryTree *l = nullptr, BinaryTree*r = nullptr) :key(k), data(d), left(l), right(r) {}
    bool operator>(BinaryTree &s)
    {
        return key>s.key;
    }
};
struct cmp
{
    bool operator()(BinaryTree *a, BinaryTree *b)
    {
        return (*a)>(*b);
    }
};
BinaryTree * huffman(int *f, char *d, int n)
{
    priority_queue<BinaryTree*, vector<BinaryTree*>, cmp> q;
    for (int i = 0; i<n; i++)
    {
        q.push(new BinaryTree(f[i], d[i]));
    }
    while (q.size()>1)
    {
        BinaryTree *x, *y, *z;
        x = q.top();
        q.pop();
        y = q.top();
        q.pop();
        z = new BinaryTree(x->key + y->key, '*', x, y);
        q.push(z);
    }
    return q.top();
}
void printCode(BinaryTree *s, string c)
{
    if (s->left != nullptr)
    {
        printCode(s->left, c + "0" );
    }
    if (s->right != nullptr)
    {
        printCode(s->right,  c + "1" );
    }
    if (s->left == nullptr && s->right == nullptr)
    {
        cout << s->data << " " << s->key << " " << c << endl;
    }
}

int main()
{
    int f[] = { 45,13,12,16,9,5 };
    char d[] = { 'a','b','c','d','e','f' };
    priority_queue<BinaryTree*, vector<BinaryTree*>, cmp> q;
    int i;
    for (i = 0; i < 6; i++)
    {
        q.push(new BinaryTree(f[i], d[i]));
    }
    BinaryTree *h = huffman(f, d, 6);
    printCode(h, "");
}
  • 1
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 6
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 6
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值