2018.1.27 T2 尤格萨隆的合并石子大作战

今天的题好像并没有那么难,可惜就两个人得分了。


【题面】

题目描述1
题目描述2
数据范围


【思路】

显然是一个多岔哈弗曼树,我们知道哈弗曼树的时间复杂度最优可以做到O(n),但是这次我们显然要做n次哈弗曼树,理论上的时间复杂度是 O(n2) O ( n 2 ) 的,不过看到数据范围感觉好像只有 O(nlogn) O ( n log ⁡ n ) 才能过。

引理:

limn+i=1n1i=lnn+γ lim n → + ∞ ∑ i = 1 n 1 i = ln ⁡ n + γ

其中: γ0.57721566490153286060651209... γ ≈ 0.57721566490153286060651209...

也就是有:

limn+i=1nni=nlnn+γn=O(nlogn) lim n → + ∞ ∑ i = 1 n n i = n ln ⁡ n + γ ⋅ n = O ( n log ⁡ n )

也就是说,如果我们能把k岔哈弗曼树的时间复杂度优化到 O(nk) O ( n k ) 那么就有总时间复杂度为 O(nlogn) O ( n log ⁡ n )

单调队列法,考虑到单调队列里的元素最多只有 O(nk) O ( n k ) 个,那么我们可以先假定只在原序列中取k个元素,然后把这k个元素中的最后一个元素与队首比较。如果队首更优,那就把队首统计到本次合并的答案中,这样的话我们还需要在原序列中回退一个元素…依此类推。

为什么这样找时间复杂度能更优呢?因为原序列永远不变,所以我们最开始可以先把它排个序,然后求一下前缀和,用以维护区间和。这个预处理时间复杂度是 O(nlogn) O ( n log ⁡ n ) 的(sort排序+前缀和)。然后假定在某种状态下单调队列里没有元素,那么我们只需要取原序列中的前k个元素。因为我们预处理了前缀和,我们可以 O(1) O ( 1 ) 求出前k个元素的和。

因为我们最多执行 O(nk) O ( n k ) 此操作就能把所有元素合并成一堆,所以单调队列中的元素最多只会有 O(nk) O ( n k ) 个,我们最多回退 O(nk) O ( n 所 以 k ) 次。制造一个k岔哈弗曼树的时间复杂度就是 O(nk) O ( n k ) 的。

因此,程序的总时间复杂度就如引理中所叙述的 O(nlogn) O ( n log ⁡ n )

另外要注意的一点是,k岔哈弗曼树在运行之前需要对原数据进行补“0”。k岔哈弗曼树每次减少(k-1)个元素,那么我们就得把元素个数补成刚好大于等于原长度的第一个除(k-1)余1的数。


【代码】

#include <queue>
#include <cstdio>
#include <cctype>
#include <cstdlib>
#include <cstring>
#include <algorithm>
using namespace std;

const int maxn = 200000 + 10, inf = 0x7f7f7f7f;
bool icmp(int a,int b){return a < b;}

class array_item{
    int a[maxn], pre[maxn], n;
    inline int getint(){
        int ans=0;bool flag=0;char c=getchar();
        while(!isdigit(c)){flag|=c=='-';c=getchar();}
        while( isdigit(c)){ans=ans*10+c-'0';c=getchar();}
        return flag?-ans:ans;
    }
    public:
        inline int operator[](int idx){
            if(idx > n || idx <= 0) return inf;
            return a[idx];
        }
        inline int sum(int L,int R){
            return pre[R] - pre[L-1];
        }
        inline void input(const int n){
            this->n = n;
            pre[0]=0;
            for(int i=1; i<=n; i++) a[i]   = getint();
            sort(a+1, a+n+1, icmp);
            for(int i=1; i<=n; i++) pre[i] = pre[i-1] + a[i];
        }
}array;

class minor{
    int ahead,k,n,ans;
    queue<int>Q;
    inline int size(){return n - ahead + 1 + Q.size();}
    inline int run(){
        int lst = ahead + k - 1;
        int sum = 0;
        while(!Q.empty() && array[lst] > Q.front()){
            sum += Q.front(); Q.pop(); lst--;
        }
        sum += array.sum(ahead,lst); ahead = lst + 1;
        Q.push(sum); ans += sum;
        return sum;
    }
    public:
        void init(const int k,const int n){
            this->k = k; this->n = n;
            int zcnt =((k-1) - n%(k-1) + 1)%(k-1); //put zero
            int first = k - zcnt; ahead = first + 1;
            if(first){
                ans = array.sum(1,first);
                Q.push(ans);
            }else ans = 0;
        }
        int solve(){
            while(size()>1) run();
            while(!Q.empty()) Q.pop();
            return ans;
        }
}launcher;

int main(){
    freopen("stone.in", "r",stdin);
    freopen("stone.out","w",stdout);
    int n; scanf("%d", &n);
    array.input(n);
    for(int i=2; i<=n; i++){
        launcher.init(i,n);
        int ans=launcher.solve();
        printf("%d\n",ans);
    }
    return 0;
}

这道题我一遍过样例,然后就A了,非常开心。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值