• 生日礼物


    生日礼物

    给出长度为n的序列({a_i}),请从中选出不超过m段,最大化每段的和的和,其中(1≤n,m≤10^5,|a_i|≤10^4)

    法一:递推

    这是一道区间划分的问题,容易想到设一个方程(f[i][j])表示前i数字,选出j段的最大的每段的和的和,容易有

    (f[i][j]=max(f[i-1][j],f[i-1][j-1]+a_i))

    但是无法优化,于是萎了,参考代码就不给出了。

    法二:贪心

    首先可以简化问题,也就是把同符号的相邻的一段合并成一个数字,不妨记进行这个操作以后的序列为({b_i}),长度为l,注意到它的一个性质,正负是交错的。

    此时如果正数少于m,这道题目就做完了,接下来对大于m来考虑

    然后根据最优性贪心,我们的选择一个极值来考虑问题,这个极值也就是绝对值的最小值,不妨设选出来的数为(b_i),我们可以得到以下结论

    • 该数字在边界,也就是(i=1 or i=l)
    1. (a_i<0),此时显然不会有人没事去选一个负数,于是可以将它从序列中删去,删去这个操作可以利用链表来实现

    2. (a_i>0),此时该数为正数中最小的数字,因为其绝对值最小,不可能单独选它,因为换成别的数字,会使结果更加优秀,如果需要将其与其他的正数合并成一段的话,又必须要选中间的负数,然而显然这个负数加上(a_i)绝对是小于0,于是答案中不这样操作,会让结果更加优秀。

    于是我们可以得到结论,如果(a_i)在边界,我们应该删去它。

    • 该数字在中间
    1. (a_i<0) 此时容易知道,它的绝对值必然是最小的,显然不会单独去选它,而是通过它合并相邻的两个正数,如果单独选两边的正数,显然可以选择的段数会减2,而选了两边的正数再加上这一个负数,可以选择的段数会减1,而此时再随便选择一段序列中的一个正数,必然与这个负数的和都是大于等于0的,因此容易知道后者的决策绝对不会比前者差劲。

    2. (a_i>0),此时容易知道它是最小的正数,单独选择它,换成别的正数结果会更加优秀,你也不会没事单独选择两边的负数,于是只能将两边的负数与之合并。

    因此根据以上,我们可以得到,如果是一个绝对值最小的在中间的数字,我们就将两边的数字与之合并,然后正数的段数显然减少了1,而如果在边界,直接删去,如果删去的是正数,那么正数的段数减少了1,最终合并到正数的段数少于m,此时就可以直接选择,寻找绝对值最小利用优先队列,合并和删去利用链表,顺便优先队列中记录这个元素在链表中对应的位置,这和原问题是等价的,是一个特别难的合并性贪心,最终时间复杂度(O(nlog(n)))

    参考代码:

    #include <iostream>
    #include <cstdio>
    #define il inline
    #define ri register
    #define ll long long
    #define Size 101000
    #define intmax 0x7fffffff
    using namespace std;
    template<class free>
    struct heap{
    	free a[Size];int n;
    	il void push(free x){
    		a[++n]=x;ri int p(n);
    		while(p>1)
    			if(a[p]<a[p>>1])
    				swap(a[p],a[p>>1]),
    					p>>=1;
    			else break;
    	}
    	il void pop(){
    		a[1]=a[n--];ri int p(1),s(2);
    		while(s<=n){
    			if(s<n&&a[s+1]<a[s])++s;
    			if(a[s]<a[p])
    				swap(a[s],a[p]),
    					p=s,s<<=1;
    			else break;
    		}
    	}
    };
    template<class free>
    struct list{
    	struct iter{
    		iter*pre,*next;free v;
    	}*head,*tail,*lt;
    	il void initialize(){
    		head=new iter(),tail=new iter();
    		head->next=tail,tail->pre=head;
    	}
    	il void insert(iter *p,free v){
    		lt=new iter{p->pre,p,v};
    		p->pre->next=lt,p->pre=lt;
    	}
    	il void erase(iter *p){
    		p->next->pre=p->pre;
    		p->pre->next=p->next;
    		p->v=intmax;
    	}
    };
    struct hl{
    	int d;list<int>::iter *p;
    	il bool operator<(const hl&x)const{
    		return abs(d)<abs(x.d);
    	}
    };
    heap<hl>H;
    list<int>L;
    list<int>::iter *p;
    int a[Size];
    il void read(int&);
    int main(){L.initialize();
    	int n,m,cnt(0),ans(0);read(n),read(m);
    	L.insert(L.tail,0),p=L.tail->pre;
    	for(int i(1),a;i<=n;++i){
    		read(a);if((ll)p->v*a>=0)p->v+=a;
    		else{
    			if(p->v>0)++cnt;
    			L.insert(p->next,a);
    			H.push({p->v,p});
    			p=p->next;
    		}
    	}if(p->v>0)++cnt;H.push({p->v,p});
    	while(cnt>m){
    		while(p=H.a[1].p,p->v==intmax)H.pop();
    		if(p->pre==L.head||p->next==L.tail){
    			if(H.a[1].d>0)--cnt;
    			H.pop(),L.erase(p);continue;
    		}--cnt;
    		p->v+=p->pre->v+p->next->v;
    		L.erase(p->pre),L.erase(p->next);
    		H.pop(),H.push({p->v,p});
    	}for(int i(1);i<=H.n;++i)
    		if(H.a[i].p->v!=intmax&&H.a[i].d>0)
    			ans+=H.a[i].d;
    	printf("%d",ans);
    	return 0;
    }
    il void read(int &x){
    	x^=x;ri char c;while(c=getchar(),c==' '||c=='
    '||c=='
    ');
    	ri bool check(false);if(c=='-')check|=true,c=getchar();
    	while(c>='0'&&c<='9')x=(x<<1)+(x<<3)+(c^48),c=getchar();
    	if(check)x=-x;
    }
    
    
  • 相关阅读:
    如何用grep命令同时显示匹配行上下的n行 (美团面试题目)
    Maven面试宝典
    Java经典设计模式 总览
    Java设计模式之工厂模式
    Java设计模式
    三次握手,四次挥手 具体发送的报文和状态都要掌握(阿里)
    运动与饮食结合
    健身计划
    Java中的多线程=你只要看这一篇就够了
    js禁止复制粘贴
  • 原文地址:https://www.cnblogs.com/a1b3c7d9/p/11257844.html
Copyright © 2020-2023  润新知