枚举、前缀和、二分

题意:

有m种花,每种花数量无上限。
已知对于第i种花,第一次选收获a[i].
此后,再次选第i种花收获b[i].
现在要选n种花,请问收获的最大值是多少?
1 <= n <= 10^9 , 1 <= m <= 10^5 , 0 <= a[i],b[i] <= 10^9

分析:

这一题,乍一看很像动态规划类似背包问题。但是状态却很难表示。
因此,我并没有尝试动态规划。
下面给出我的想法:

我们直观地想想,我们应该如何拿花:

我们先对所有的a和所有的b进行一次排序:
假设排成这样(从大到小,数值相等a放在前面)
[a5,a4,a9,a1,b4,a7,b5,b6,b5,b9,a2。。。。。。]
我们很明显前四个应该选择a5,a4,a9,a1
然后到第五个b4
因为前面a4被选过了所以我们不需要考虑任何也可以选b4
并且因为b4比后面的花收获都高了,所以我们就把接下来的所有都选择为b4吧

这是很显然的!

但是倘若在前面a4并没有选到呢?

[a5,a9,a1,b4,a7,b5,a4,b6,b5,b9,a2。。。。。。]
b4 变成了第四个。
如此当我们遇到b4时就面临了分歧
1.舍弃b4向后选
2.到后面去先把a4选了,然后再回来以后都选b4

这两种分歧视具体情况没有最优的。
比如b4 = 5,a4 = 1, a7 = 4,b5 = 4(a5之前选过了) n = 5
如此我们应该选择2
而对于上面的例子,我们别的不变单单让n = 1000如此我们就一定要选择1了

这种分歧我们是无法判定的!!
因此采取贪心策略,贪心的向后选是肯定不行的!!!!!

但是通过上面的分析,我们可以观察到一个重点:
就是:
我们最后一般都会在一个b停下来,然后接下来的所有次数都选他。
尤其是在n>m,要选的数目大于种类时,这是一定发生的。

例外则是
[a1,a2,a3,a4,a6,a5,a7,b1,b4,b3,b6。。。。。] 从大到小排序
n = 6 <= m
唯有这种情况我们才可能选择雨露均沾式取法。
当然如果这样[a1,b1,a3,a4,a6,a5,a7,b2,b4,b3,b6。。。。。]
的话仍然不会发生

如此,我们找到了什么?
枚举点!!!!!
我们枚举每一个b点,计算以他作为最后选择的所得到的收获。
如果n<=m,我们再将前n大的a相加进行一次比较。
最后选出最大值
如此,便可以了!!!!!!

那么重点便是枚举一个bi点,如何计算以他作为最后选择的收获?
根据我们之前的分析,我要以bi为最后选择,那么之前比bi大的a我肯定都选了 假设共x个
然后,要选择b,我需要先判断ai是否已经被选。
如果已经被选,那么接下来我就选择n-x个bi就好了
如果没有被选,那么我们先去选ai,然后再选n-x-1个bi

有了思路接下来就是编程的问题了,但是变成真的是个问题!!!

我打算先对a进行排序,从大到小
然后计算出其前缀和sum
如果n<=m的话,我们初始ans = sum[n]
再枚举bi时,我使用二分算法,找到bi在数组中的位置,也就是有多少个a比bi大
然后利用前缀和sum直接得出x个a的值
之后判断ai是否已经被选?这该如何判断?
其实只要判断对于每一朵花是a是否大于等于b就好了
因为若a>=b那么二分查找后a肯定在b的前面,也就是已经被选了!!
最后在于ans比较,留下大的
如此,我们成功全部解决了!

还需要的是,上述的需要的二分查找,因为我们是在一个递减序列中找第一个小于目标的元素
所以C++中自带的不是太好用,我重新写了一个二分查找。需要对二分查找模板足够的熟悉!!!!!

其实是很麻烦的。。。。。。

代码如下:

#include<iostream>
#include<algorithm>
#include<functional>
using namespace std;
typedef long long ll;
typedef pair<int, int> pii;
const int max_m = 1e5 + 100;
pii a[max_m];
pii b[max_m];
int c[max_m];
ll sum[max_m];
bool jud[max_m];
ll n, m;

int bound(int l, int r, pii target) {
    while (l < r) 
    {
        int mid = (l + r) >> 1;
        if (a[mid] > target)l = mid + 1;
        else r = mid;
    }
    return r - 1;
}

int main() {
    ios::sync_with_stdio(0);
    int t;cin >> t;
    while (t--) {
        cin >> n >> m;
        fill(jud + 1, jud + 1 + m, false);
        for (int i = 1;i <= m;i++)
        {
            cin >> a[i].first;
            a[i].second = i;
            cin >> b[i].first;
            b[i].second = i;
            c[i] = a[i].first;
            if (a[i].first >= b[i].first)
                jud[i] = true;
        }
        sort(a + 1, a + 1 + m, greater<pii>());
        for (int i = 1;i <= m;i++)
        {
            sum[i] = sum[i - 1] + a[i].first;
        }

        ll ans = 0;
        if (n <= m)ans = sum[n];

        for (int i = 1;i <= m;i++)
        {
            ll comp = 0;
            int id = bound(1, 1 + m, b[i]);
            if (id >= n)continue;
            comp = sum[id];
            if (jud[b[i].second])
            {
                comp += b[i].first * ((ll)n - id);
            }
            el***p += c[b[i].second] + ((ll)n - id - 1) * b[i].first;
            }
            ans = max(ans, comp);
        }
        cout << ans << endl;
    }
}

注意!
有一行代码,不可或缺!

            int id = bound(1, 1 + m, b[i]);
            if (id >= n)continue;//!!!!!!!!!!

因为我们一共就要取n个,而在这里比bi大的a就已经大于等于n个了,所以我们是不可能取到bi的。在有的题目当中即使不注意这些鸡毛蒜皮的事情也无关紧要,但这里不一样。

            ll comp = 0;
            int id = bound(1, 1 + m, b[i]);
            if (id >= n)continue;
            comp = sum[id];
            if (jud[b[i].second])
            {
                comp += b[i].first * ((ll)n - id); //!!!!!!!1
            }
            el***p += c[b[i].second] + ((ll)n - id - 1) * b[i].first;//!!!!!!
            }

我们算入了sum[id],id>=n,虽然后面的n-id是负的,但是无法保证他们加起来会小于ans,不给ans造成影响。

因此,如果你少了这句话,你将在第五个测试用例报错!!!!
滑稽。。。。。。