贪心算法与递归

贪心算法

  分金条问题

  会议室场次问题

  花费资金做项目问题

  取中位数问题

  字典序最小问题

 

暴力递归

  汉诺塔问题

  打印字符串的全部子序列

  打印字符串全排列(有重复)

  打印字符串全排列(无重复)

  数字转字符串

  背包问题

  纸牌问题

  N皇后问题

 

 

 

分金条问题

一块金条切成两半,是需要花费和长度数值一样的铜板的。比如长度为20的金 条,不管切成长度多大的两半,都要花费20个铜板。
一群人想整分整块金条,怎么分最省铜板? 例如,给定数组{10,20,30},代表一共三个人,整块金条长度为10+20+30=60。 金条要分成10,20,30三个部分。 如果先把长度60的金条分成10和50,花费60; 再把长度50的金条分成20和30,花费50;一共花费110铜板。 但是如果先把长度60的金条分成30和30,花费60;再把长度30金条分成10和20, 花费30;一共花费90铜板。 输入一个数组,返回分割的最小代价

 

思路:

首先将数组的值进到优先级队列里去,有一个小到大的排序过程,然后依次取最大的两个值进行计算代价

然后将计算完成之后的结果再放入队列尾部,不断重复,一直到队列为空。

每次累加最后返回的值,就是分割的最小代价。

int lessMoney(int array[],int len)
{
    priority_queue<int> pQ;
    for (int i = 0; i < len; i++)
    {
        pQ.push(array[i]);
    }
    int sum = 0;
    int cur = 0;
    int a = 0;
    while (pQ.size() > 1)
    {
        a = pQ.top();
        pQ.pop();
        cur = a + pQ.top();
        pQ.pop();
        sum += cur;
        pQ.push(cur);
    }
    return sum;
}

 

 

会议室场次问题

一些项目要占用一个会议室宣讲,会议室不能同时容纳两个项目的宣讲。 给你每一个项目开始的时间和结束的时间(给你一个数组,里面是一个个具体的项目),你来安排宣讲的日程,要求会议室进行的宣讲的场次最多。 返回这个最多的宣讲场次。

 

思路:

首先定义项目类,开始时间和结束时间,按结束时间进行从小到大的排序

start是可以开始的时间,传参进来为0,从0点开始。

如果start的时间是在当前的项目的时间之前或者刚刚好,则可以开展的场次结果加1,并且将这次结束时间作为下次可开始的时间。

class Program
{
public:
    Program(int s,int e):start(s),end(e){}
    int start;
    int end;
};

bool compare(Program a, Program b)
{
    return a.end < b.end;
}

int bestArrange(Program program[], int start)
{
    int len = sizeof(program) / sizeof(program[0]);
    sort(program, program + len, compare);
    int result = 0;
    for (int i = 0; i < len; i++)
    {
        if (start <= program[i].start)
        {
            result++;
            start = program[i].end;
        }
    }
    return result;
}

 

花费资金做项目问题

输入:

正数数组costs

正数数组profits

正数k

正数m

含义:

costs[i] 表示i号项目的花费,profits[i] 表示i号项目在扣除花费之后还能挣到的钱(利润) ,k表示你只能串行的最多做k个项目 ,m表示你初始的资金

说明: 你每做完一个项目,马上获得的收益,可以支持你去做下一个项目。

输出: 你最后获得的最大钱数。

 

思路:

首先定义一个类,包含花费和利润。

然后建立一个花费的小根堆,一个利润的大根堆。为了后续花最少的钱赚更多的钱

然后依次将花费表和利润表放入小根堆里去。

然后根据最多做的项目数量来进行循环,在小根堆不为空的情况下把所有买得起的全扔进利润大根堆里面去。

然后进行资金的累加,将大根堆第一个的利润直接加到初始资金上。

有一个边界考虑,当大根堆为空的时候,然后还没有达到做大次数,这个时候就说明了钱不够,再怎么样也做不完所有项目,所以直接返回当前的资金就可以了

class Node
{
public:
    Node(int p,int c):profit(p),cost(c){}
    int cost;
    int profit;
};

int findMaximizedCapital(int k, int w, int profits[], int capital[],int len)
{
    priority_queue<Node*,vector<Node*>,greater<Node*>> minCostQ;//小根堆
    priority_queue<Node*,vector<Node*>,less<Node*>>maxProfitQ;//大根堆
    for (int i = 0; i < len; i++)
    {
        minCostQ.push(new Node(profits[i], capital[i]));
    }
    for (int i = 1; i <= k; i++)
    {
        while (!minCostQ.empty() && minCostQ.top()->cost <= w)
        {
            maxProfitQ.push(minCostQ.top());
            minCostQ.pop();
        }
        if (maxProfitQ.empty())
        {
            return w;
        }
        w += maxProfitQ.top()->profit;
        maxProfitQ.pop();
    }
    return w;
}

 

 

取中位数问题

一个数据流中,随时可以取得中位数

 

思路:

用一个小根堆和一个大根堆进行不断的比较交换,最后获得中位数。

第一个值直接进大根堆,剩下的值如果是小于等于大根堆的堆顶,就进入大根堆,否则进入小根堆。

我画了一个图来表明上面的这个操作,我用的队列来画的图,这个不是堆的图。只是为了看的更加直观。

从这个图可以很直观的看出来,比大根堆小的值就让他沉下去,比大根堆堆顶大的值就放入小根堆然后让他沉下去。

相当于两个极端,一大一小都沉下去,堆顶就是中间的部分。

如果当大根堆或者小根堆里面的元素个数相差2个,就从元素多的那个堆中顶部取一个值放入元素少的那个堆中。

最后判断两个堆中的元素个数是否一样,如果一样就两个顶部相加除2。

如果不一样,就直接取元素多的那个堆的顶部

priority_queue<int, vector<int>, less<int>>maxHeap;
priority_queue<int, vector<int>, greater<int>>minHeap;

void heapChange()
{
    if (maxHeap.size() == minHeap.size() + 2)
    {
        minHeap.push(maxHeap.top());
        maxHeap.pop();
    }
    if (minHeap.size() == maxHeap.size() + 2)
    {
        maxHeap.push(minHeap.top());
        minHeap.pop();
    }
}

void addNumber(int num)
{
    if (maxHeap.empty() || num <= maxHeap.top())
    {
        maxHeap.push(num);
    }
    else
        minHeap.push(num);
    heapChange();
}

int getMid()
{
    int maxSize = maxHeap.size();
    int minSize = minHeap.size();
    if (maxSize == 0 && minSize == 0)
    {
        return 0;
    }
    int minHeapHead = minHeap.top();
    int maxHeadHead = maxHeap.top();
    if (maxSize == minSize)
    {
        return (minHeapHead + maxHeadHead) >> 1;
    }
    return maxSize > minSize ? maxHeadHead : minHeapHead;
}

 

字典序最小问题 

给定长度为N的字符串S,要构造一个长度为N的字符串T。起初,T是一个空串,随后反复进行下列任意操作。

从S的头部删除一个字符,加到T的尾部,从S的尾部删除一个字符,加到T的尾部

目标是要构造字典序尽可能小的字符串T

 

思路:

从左往右的模型,定义两个变量L和R,分别等于0和字符串长度-1

当L小于等于R的时候进行循环,定义一个标志,默认为左边小,如果左边字符等于右边字符不变。

当L加上 i 小于等于 R的时候,判断 L+ i 是否小于R,如果小于 标志位改为true,如果大于 标志位改成 false

最后如果标志位是true 就打印左边的字符,如果是false就打印右边的字符。如果左右两边字符一样,标志位没有改变,输出任意字符都可以

int N = 6;
string str = "abcdbc";
void solve()
{
    int L = 0, R = N - 1;
    bool left = false;
    while (L <= R)
    {
        for (int i = 0; L + i < R; i++)
        {
            //如果首字符小于尾字符就打印首字符
            if (str[L + i] < str[R - i])
            {
                left = true;
                break;
            }
            else if (str[L + i] > str[R - i])
            {
                left = false;
                break;
            }
        }
        if (left)
        {
            cout << str[L++];
        }
        else
            cout << str[R--];
    }
}

 

汉诺塔问题

思路:

假设三根柱子,分别为a,b,c,当只有a只有一层的时候,直接从a移到c就可以了

当如果有n层的时候,a要想移到c,得把a上面n-1层都先移到b柱子上放着,才能把a最底层移到c去

n-1都移到了b柱子上后,还是得把b柱子上的n-1层移到另一个与目的c柱子无关的柱子上去,所以把b上面的n-1移到a上去,然后再把最后一层从b移到c

void process(int n, char from, char to, char other)
{
    if (n == 1)
    {
        cout << from << "->" << other << endl;
    }
    else if (n > 1) {
        process(n - 1, from, other, to);
        cout << from << "->" << other << endl;
        process(n - 1, other, to, from);
    }
}

 

 

打印字符串的全部子序列

打印一个字符串的全部子序列,包括空字符串

 

思路:

核心是用列表来装子序列,最后打印结果。

首先传入一个字符串,然后进行是否为空的判断,然后调用process递归

参数为原字符串,下标从0开始,走过的路径path默认为空串,答案列表容器ans

如果坐标index等于字符串的长度,就是说明已经到尾了,直接将路径传入答案列表中。

当还没到末尾的时候,就进行下标的加1,然后两个递归。

第一个递归是不选择当前字符,第二个递归是选择当前字符,并把当前字符加到路径上。

最后递归结束之后返回ans给main函数,打印ans容器,出来的结果是有重复的子序列。

void process(string str, int index, string path, list<string>& ans)
{
    if (index==str.length())
    {
        ans.push_back(path);
    }
    else
    {
        process(str, index + 1, path, ans);
        process(str, index + 1, path + str[index], ans);
    }
}

list<string>* allSubseq(string str)
{
    list<string>*ans = new list<string>;
    if (str.empty())
    {
        return ans;
    }
    if (str.length() == 0)
    {
        ans->push_back("");
        return ans;
    }
    process(str, 0, "", *ans);
    return ans;
}

 

 

打印字符串全排列(有重复)

打印一个字符串的全部排列

 

思路:

首先将数组里面的字符串放入到vector容器中,然后进行递归操作。

先判断容器中是否为空,如果为空说明路径已经搜集完了,将路径放入答案列表中。

//全排列有重复
void process(vector<char>&set, string path, list<string>& ans)
{
    if (set.empty())
    {
        ans.push_back(path);
        return;
    }
    for (int index = 0; index < set.size(); index++)
    {
        string pick = path + set[index];
        vector<char>next;
        next.assign(set.begin(), set.end());
        auto del = next.begin() + index;
        next.erase(del);
        process(next, pick, ans);
    }
}

list<string>* allSort(string str)
{
    list<string>* ans = new list<string>;
    vector<char> set;
    for (int i = 0; i < str.length(); i++)
    {
        set.push_back(str[i]);
    }
    process2(set, "", *ans);
    return ans;
}

 

 

打印字符串全排列(无重复)

打印一个字符串的全部排列,要求不要出现重复的排列

 

思路:

首先因为需要排列,所以将字符串弄到vector容器中装着,然后调用函数process进行递归。

传入参数,装字符串的容器,路径以及空的答案列表容器

当字符容器中为空的时候,代表路径已经形成,直接将路径传入ans容器中就可以了

如果不为空,就先建立一个无序表来避免重复的元素

然后进行字符容器的遍历,如果当前字符在无序表中没有就把当前字符加入无序表,并且路径上添加当前字符

然后再用临时的一个字符容器把原来的字符容器的值全拷贝,并且把当前字符删掉,从下一个字符开始,然后用临时容器作为递归参数。

//全排列无重复
void process2(vector<char>&set, string path, list<string>&ans)
{
    if (set.empty())
    {
        ans.push_back(path);
        return;
    }
    unordered_set<char> picks;
    for (int index = 0; index < set.size(); index++)
    {
        if (!picks.count(set[index])) 
        {
            picks.insert(set[index]);
            string pick = path + set[index];
            vector<char> next;
            next.assign(set.begin(), set.end());
            auto del = next.begin() + index;
            next.erase(del);
            process2(next, pick, ans);
        }
    }
}
list<string>* allSort(string str)
{
    list<string>* ans = new list<string>;
    vector<char> set;
    for (int i = 0; i < str.length(); i++)
    {
        set.push_back(str[i]);
    }
    process2(set, "", *ans);
    return ans;
}

 

数字转字符串

规定1和A对应、2和B对应、3和C对应... 那么一个数字字符串比如"111",就可以转化为"AAA"、"KA"和"AK"。 给定一个只有数字字符组成的字符串str,返回有多少种转化结果

 

思路:

首先传入数字字符串,然后进行一个空串的判断,再进行process函数的调用。

终止条件,当下标i等于长度的时候,这时候就为一个有效的结果,返回1

如果当前数字字符为0,就说明当前字符不能决定是否要不要,直接返回0

当前字符为1开头的时候,得到一种结果,然后看字符串的下一个是否存在,如果存在就直接结果累加。

当前字符为2开头的时候,因为是26个字符,所以判断第二个字符是否有效的时候要加上限定,第二个字符是否在0到6之间。

最后外层的返回调用是指3至9的单个字符,不是1开头也不是2开头

int process(string str, int i)
{
    if (i == str.length())
    {
        return 1;
    }
    if (str[i] == '0')
    {
        return 0;
    }
    if (str[i] == '1')
    {
        int res = process(str, i + 1);
        if (i + 1 < str.length())
        {
            res += process(str, i + 2);
        }
        return res;
    }
    if (str[i] == '2')
    {
        int res = process(str, i + 1);
        if (i + 1 < str.length() && str[i + 1] >= '0'&&str[i + 1] <= '6')
        {
            res += process(str, i + 2);
        }
        return res;
    }
    return process(str, i + 1);
}

int number(string str)
{
    if (str == "" || str.length() == 0)
    {
        return 0;
    }
    return process(str, 0);
}

 

背包问题

给定两个长度都为N的数组weights和values,weights[i]和values[i]分别代表 i号物品的重量和价值。给定一个正数bag,表示一个载重bag的袋子,你装的物 品不能超过这个重量。返回你能装下最多的价值是多少?

 

思路1:

传参分别是 weight 装重量的容器,value 装价值的容器,alreadyW 已经有的重量,bag 包的能装的重量大小

终止条件是已有的重量大于包能承受的重量

用两个递归解决,p1 的意思是不加当前重量的递归,p2next 是加下一个重量的递归

在这里使用一个p2next目的是为了防止下一个重量不存在,如果不存在p2next的值会变成-1

然后进行判断,当下一个重量存在的时候,就形成p2

最后进行比较最大值返回   p1不要当前货物形成的最大价值,p2要当前货物形成的最大价值

int process(vector<int>& w, vector<int>& v, int index, int alreadyW, int bag)
{
    if (alreadyW > bag)
    {
        return -1;
    }
    if (index == w.size())
    {
        return 0;
    }
    int p1 = process(w, v, index + 1, alreadyW, bag);
    int p2next = process(w, v, index + 1, alreadyW + w[index], bag);
    int p2 = -1;
    if (p2next != -1)
    {
        p2 = v[index] + p2next;
    }
    return max(p1, p2);
}

int maxValue(vector<int> w, vector<int> v, int bag)
{
    return process(w, v, 0, 0, bag);
}

 

思路2:

传参分别是 weight 重量,value 价值,rest 剩余空间

当剩余空间小于或者等于0时,或者 下标走到最后的位置的时候,直接返回0

p1 是不要当前货物进行的递归,当剩余空间大于等于当前的重量的时候,用p2 得到要当前货物的最大价值,后面process里面 rest - w[index] 是把剩余空间减去了当前货物的重量 

最后再求p1 和 p2 的最大值

int process(vector<int> w, vector<int> v,int index, int rest)
{
    if (rest <= 0)
    {
        return 0;
    }
    if (index == w.size())
    {
        return 0;
    }
    int p1 = process(w, v, index + 1, rest);
    int p2 = -1;
    if (rest >= w[index])
    {
        p2 = v[index] + process(w, v, index + 1, rest - w[index]);
    }
    return max(p1, p2);
}

int maxValue(vector<int> w, vector<int> v, int bag)
{
    return process(w, v, 0, bag);
}

 

 

纸牌问题

给定一个整型数组arr,代表数值不同的纸牌排成一条线。玩家A和玩家B依次拿走每张纸 牌,规定玩家A先拿,玩家B后拿,但是每个玩家每次只能拿走最左或最右的纸牌,玩家A 和玩家B都绝顶聪明。请返回最后获胜者的分数。 【举例】 arr=[1,2,100,4]。 开始时,玩家A只能拿走1或4。如果开始时玩家A拿走1,则排列变为[2,100,4],接下来 玩家 B可以拿走2或4,然后继续轮到玩家A... 如果开始时玩家A拿走4,则排列变为[1,2,100],接下来玩家B可以拿走1或100,然后继 续轮到玩家A... 玩家A作为绝顶聪明的人不会先拿4,因为拿4之后,玩家B将拿走100。所以玩家A会先拿1, 让排列变为[2,100,4],接下来玩家B不管怎么选,100都会被玩家 A拿走。玩家A会获胜, 分数为101。所以返回101。 arr=[1,100,2]。 开始时,玩家A不管拿1还是2,玩家B作为绝顶聪明的人,都会把100拿走。玩家B会获胜, 分数为100。所以返回100。

 

思路:

分别写了两个函数相互嵌套,f 代表 frist , s 代表 second ,先手和后手函数

首先在 f 函数中,i 代表最左边的位置,j 代表最右边的位置

如果 i 位置与 j 位置相同,代表现在只有一个值,那么就直接返回就可以了,返回 i 位置的值

先手和后手是一套过程,所以说这个过程。

写成函数应该是 当拿 i 位置上的值的时候,后手函数调用 ,s 函数的范围必然是从 i + 1位置上到 j 上,因为 i 位置的值已经被取走了。

反之,取 j 位置上的值,s 函数的范围 必然是 i 的位置 到 j - 1 的位置,因为 j 位置的值被取走了。

后手函数中里面,当 i 位置与 j 位置相等,说明只剩1个值了,后手函数拿不到这个值,所以返回 0 

最后的返回最小值函数中,就直接调用先手函数取值 。

返回最小值是因为,先手必然拿最大值,剩下的只有最小值给后手拿 或者刚好与先手的值一样,如果是一样的话最大值最小值无所谓,这个不在考虑范围内。

在 s 函数中,返回 f 函数,只是为了去取值,因为第二个人取值的时候,也是像第一个人一样,去取最大值。

int s(vector<int>& array, int i, int j)
{
    if (i == j)
    {
        return 0;
    }
    return min(f(array, i + 1, j), f(array, i, j - 1));
}

int f(vector<int>& array, int i, int j)
{
    if (i == j)
    {
        return array[i];
    }
    return max(array[i] + s(array, i + 1, j), array[j] + s(array, i, j - 1));
}

 

N皇后问题

 N皇后问题是指在N*N的棋盘上要摆N个皇后,要求任何两个皇后不同行、不同列, 也不在同一条斜线上。 给定一个整数n,返回n皇后的摆法有多少种。 n=1,返回1。 n=2或3,2皇后和3皇后问题无论怎么摆都不行,返回0。 n=8,返回92

 

思路:

用一个一维数组来表示皇后的位置,行号就是数组是下标,列号就是这个下标中的值

在过程函数process中,如果从0遍历到最后的n,则表示这是一组有效的解

在确定皇后位置的时候,首先要判断这个位置能不能使用,是否为对角线,或者与上一个皇后同列

用一个isValid函数来判断当前的位置是否有效,判断对角线相等用一个公式来表示,| y1 - x1 |  = | y2 - x2 |

也就是record中的0到 i - 1 的列 减 当前列 ,在这之前的行 减 当前行 ,然后都取绝对值 。相当于就是判断是否为45°或者135°的角

如果是的话就返回false 表示当前位置不能作为皇后的位置。

如果返回为true, 表明当前位置可以作为皇后的位置,然后直接记录,再进行递归找下一个位置。

bool isValid(int record[], int i, int j)
{
    for (int k = 0; k < i; k++)
    {
        if (record[k] == j || abs(record[k] - j) == abs(k - i))
        {
            return false;
        }
    }
    return true;
}

int process(int i, int record[], int n)
{
    if (i == n)
    {
        return 1;
    }
    int res = 0;
    for (int j = 0; j < n; j++)
    {
        if (isValid(record, i, j))
        {
            record[i] = j;
            res += process(i + 1, record, n);
        }
    }
    return res;
}

int num(int n)
{
    if (n < 0)
    {
        return 0;
    }
    int *record = new int[n];
    return process(0, record, n);
}

 

 

 

 

 

 

posted @ 2020-08-23 16:44  袁君(Louis)  阅读(601)  评论(0)    收藏  举报