zoukankan      html  css  js  c++  java
  • 51Nod 最大M子段和系列 V1 V2 V3

    前言

    (HE)(BJ)的光成功滚回家里了...这堆最大子段和的题抠了半天,然而各位(dalao)们都已经去做概率了...先%为敬。
    引流之主:老姚的博客

    最大M子段和 V1

    思路

    • 最简单的ver.数据范围在5000以内,可以考虑暴力一点的做法(O(n^3)),定义(dp)状态(dp[i][j]),递推式子:

    [dp[i][j]=max{dp[i-1][j],dp[k][j-1]}+a[i] (j-1le k<i) ]

    • 其中(i)表示序列中前(i)个元素且包含第(i)个元素,而(j)则表示划分了(j)个不同的字段。而对于(a[i])来说,要么和最后一个字段合并,要么单独成为一个字段,所以就可以得出动态转移方程。
    • 此时我们可以看出,对于第二维(j),只与(j)(j-1)有关,所以我们可以互换,使(dp[i][j])表示把序列前(j)个元素分成(i)个子段且包含(a[j])的最大子段和。此时的状态就只和上一行和左边的状态有关了,所以我们就可以把效率压到(O(m*n))
      此时的动态转移方程:

    [dp[i][j]=max{dp[i][j-1],dp[i-1][k]}+a[j] (i-1le k<j) ]

    • 还有一个要特判的就是(i=j)的时候,见代码即可。

    代码

    #include <bits/stdc++.h>
    using namespace std;
    typedef long long ll;
    const int maxn=1e4+5;
    const ll INF=1e15;
    ll a[maxn],dp[2][maxn];
    int n,m;
    
    int main(){
        scanf("%d%d",&n,&m);
    
        for(int i=1;i<=n;i++)
            scanf("%lld",&a[i]);
    
        int k=1;
    
        for(int i=1;i<=m;i++,k=k^1){
        	ll temp=-INF;
        	for(int j=i;j<=n;j++){
        		temp=max(temp,dp[k^1][j-1]);
        		if(i==j)
        			dp[k][j]=dp[k^1][j-1]+a[j];
        		else
        			dp[k][j]=max(temp,dp[k][j-1])+a[j]; 		
        	}
        }
        
        ll ans=-INF;
        for(int i=m;i<=n;i++)
        	ans=max(ans,dp[k^1][i]);//注意此处是k^1记录的才是第m行的状态
        printf("%lld
    ",ans);
    
        return 0;
    }
    

    最大M子段和 V3

    思路

    • 为什么先说V3因为V3是环形的边界处理比V2还要简单。对于V1的DP虽然效率是(O(m*n))但是显然从V2开始就已经有m接近于n的数据了,所以最差的效率为(O(n^2)),所以DP显然是不行了。这时候用可撤销贪心。
    • 可撤销贪心简单来说就是贪心时候的悔棋操作,可能最一开始选择的为最大的贡献(w[i]),但是第二轮选择可能会发现选择(w[i-1])(w[i+1])更优,那么此时的答案就应加上(w[i-1]+w[i+1]-w[i]),相当于建立了一个新的节点,同时其相邻关系也会发生变化。
    • 对于此题,我们可以把相邻的相同符号的数都加在一起(0加在哪里都一样甚至可以选择直接把0删了),这样就得到了一个正负交替的环形序列,而每个数均代表一个区间,此时设序列中正数个数为(cnt),正数的和为(ans)
      • (cntle m),则依照题目要求直接输出(ans)
      • 否则需要删减(cnt-m)个区间。
    • 考虑如何删减:
      • 可以选择直接删除一个正数。则每次删除最小的正数。
      • 也可以将一个负数和相邻两个正数合并,即将两个正数区间合并成一个。这时选择最大的负数和两边合并。
    • 但是此时不能随便删除,因为可能通过多次合并减少区间得到的答案会更优,盗老姚的数据:
      • 10 -4 3 -4 8 -100

      • 若先删除最小的正数3,则第二步要选择最大的负数-4,但是此时的3已经被删除了无法合并,因此我们要用到可撤销贪心思想。
    • 实现方法:
      • 将修改后的序列中的正数改为负数,压入大根堆(网上好多题解都说用绝对值,但实际上正变负更方便一点,如果用pair的话都不用重载运算符了)。
      • 可撤销贪心执行(cnt-m)次。

    一个不怕死的HACK

    • 老姚的代码
    #include <bits/stdc++.h>
    typedef long long LL;
    const int maxn=2e5+5;
    struct Node{
    	int id;
    	LL w;
    	Node(){};
    	Node(int x,LL y){id=x;w=y;}
    	bool operator <(const Node &a)const{
    		return w<a.w;
    	}
    };
    int n,m,N=0,L[maxn],R[maxn]; 
    LL ans=0,a[maxn],b[maxn];
    std::priority_queue<Node> q; 
    bool flag[maxn];
    void Init(){
    	scanf("%d%d",&n,&m);
    	for(int i=1;i<=n;++i){
    		scanf("%lld",&a[i]);//原始数组
    		if(!N || (a[i]>=0)!=(b[N]>=0))
    			b[++N]=a[i];//新数组,上个数和当前数符号不一样节点++ 
    		else
    			b[N]+=a[i];//符号一样,累加
    	}
    	if((b[1]>=0)==(b[N]>=0))//因为是环,b[1]和b[N]同号就合并
    		b[1]+=b[N--];//合并后新数组个数减一,记得N--
    }
    void Solve(){
    	Init();	
    	int tot=0;//记录正数个数
    	for(int i=1;i<=N;++i){//遍历新数组,压入大根堆
    		L[i]=i-1; R[i]=i+1;//初始化i的左右邻居
    		if(b[i]>0){//正数累加到答案,然后变负
    			ans+=b[i]; b[i]=-b[i]; tot++;
    		}
    		q.push(Node(i,b[i]));
    	}
    	R[N]=1; L[1]=N;//注意是环形
    	if(m>=tot){//正数个数小于m,则全部选
    		printf("%lld
    ",ans);return;
    	}
    	m=tot-m;//正数大于m则合并或删除tot-m个区间
    	while(m--){
    		Node t=q.top(); q.pop();
    		int i=t.id;
    		if(flag[i]){++m; continue;}
    		else{//可撤销贪心
    			ans+=b[i];
    			flag[L[i]]=1;
    			flag[R[i]]=1;
    			b[i]=b[L[i]]+b[R[i]]-b[i];			
    			R[L[L[i]]]=i;
    			L[R[R[i]]]=i;
    			q.push(Node(i,b[i]));//压入新点			
    			L[i]=L[L[i]];
    			R[i]=R[R[i]];			
    		}
    	}
    	printf("%lld
    ",std::max(0LL,ans));
    }
    int main(){
    	Solve();
    	return 0;
    }
    
    • 在51Nod直接AC了,不得不说51Nod真的都是随机数据。
    • 有个很明显的错误就是如果给出的序列都是一个符号,例如下面这个HACK数据:
      • Input:
        5 4
        1 1 1 1 1
        Output:
        5

      • 然而老姚的代码会输出0,问题在这行:
      • if((b[1]>=0)==(b[N]>=0))
        	b[1]+=b[N--];
        
      • 如果全为一个符号,最后(N=1),显然(b[1]=b[N]),这样会导致(N)变成(0)。如果对于全负,答案必输出0没什么影响,但是全正的话就不行了。不过也很好弄,加个特判即可。具体见代码(把老姚的代码改了改增加可读性(雾))。

    代码

    #include <bits/stdc++.h>
    using namespace std;
    typedef long long ll;
    const int maxn=2e5+10;
    
    struct Node{
    	int id;
    	ll w;
    	Node(){};
    	Node(int x,ll y){
    		id=x;w=y;
    	}
    	friend bool operator <(const Node &a,const Node &b){
    		return a.w<b.w;
    	}
    };
    
    int n,m,N;
    int L[maxn],R[maxn];
    ll ans;
    ll a[maxn],b[maxn];
    bool vis[maxn],flag=1;
    priority_queue<Node> q;
    
    void Init(){
    	scanf("%d%d",&n,&m);
    	for(int i=1;i<=n;i++){
    		scanf("%lld",&a[i]);
    
    		if(a[i]<0)flag=0;
    		else ans+=a[i];
    
    		if(!N || (a[i]>=0)!=(b[N]>=0))
    			b[++N]=a[i];
    		else 
    			b[N]+=a[i];
    	}
    	if((b[1]>=0)==(b[N]>=0))
    		b[1]+=b[N--];
    }
    
    int main(){
    	Init();
    
    	int tot=0;
    	for(int i=1;i<=N;i++){
    		L[i]=i-1;R[i]=i+1;
    		if(b[i]>0){
    			b[i]=-b[i];
    			tot++;
    		}
    		q.push(Node(i,b[i]));
    	}
    	
    	R[N]=1;L[1]=N;
    
    	if(m>=tot||flag){
    		printf("%lld",ans);
    		return 0;
    	}
    		
    	m=tot-m;
    
    	while(m--){
    		Node t=q.top();
    		q.pop();
    
    		int i=t.id;
    		if(vis[i]){
    			m++;
    			continue;
    		}else{
    			ans+=b[i];
    			q.push(Node(i,b[i]));
    			
    			vis[L[i]]=1;vis[R[i]]=1;
    			b[i]=b[L[i]]+b[R[i]]-b[i];
    			R[L[L[i]]]=i;
    			L[R[R[i]]]=i;
    			L[i]=L[L[i]];
    			R[i]=R[R[i]];
    		}
    	}
    
    	printf("%lld",max(ans,0LL));
    
    	return 0;
    }
    

    最大M子段和 V2

    • 最后就是V2了,一开始尝试特判(0)(N)处的边界但发现巨难写,后来发现其实只需要加上一个权值为负无穷的(0)号节点就行了...

    代码

    #include <bits/stdc++.h>
    using namespace std;
    typedef long long ll;
    const int maxn=5e4+10;
    
    struct Node{
    	int id;
    	ll w;
    	Node(){};
    	Node(int x,ll y){
    		id=x;w=y;
    	}
    	friend bool operator <(const Node &a,const Node &b){
    		return a.w<b.w;
    	}
    };
    
    int n,m,N;
    int L[maxn],R[maxn];
    ll ans;
    ll a[maxn],b[maxn];
    bool vis[maxn],flag=1;
    priority_queue<Node> q;
    
    void Init(){
    	scanf("%d%d",&n,&m);
    	for(int i=1;i<=n;i++){
    		scanf("%lld",&a[i]);
    		if(a[i]<0)flag=0;
    		else ans+=a[i];
    		if(!N && a[i]<0)continue;
    
    		if(!N || (a[i]>=0)!=(b[N]>=0))
    			b[++N]=a[i];
    		else 
    			b[N]+=a[i];
    	}
    
    	while(b[N]<0)b[N--]=0;
    }
    
    int main(){
    	Init();
    
    	int tot=0;
    	for(int i=1;i<=N;i++){
    		L[i]=i-1;R[i]=i+1;
    		if(b[i]>0){
    			b[i]=-b[i];
    			tot++;
    		}
    		q.push(Node(i,b[i]));
    	}
    	
    	R[N]=0;L[0]=N;R[0]=1;
    	q.push(Node(0,b[0]=-1e15));
    
    	if(m>=tot||flag){
    		printf("%lld",ans);
    		return 0;
    	}
    		
    	m=tot-m;
    
    	while(m--){
    		Node t=q.top();
    		q.pop();
    
    		int i=t.id;
    		if(vis[i]){
    			m++;
    			continue;
    		}else{
    			ans+=b[i];
    			q.push(Node(i,b[i]));
    
    			vis[L[i]]=1;vis[R[i]]=1;
    			b[i]=b[L[i]]+b[R[i]]-b[i];
    			R[L[L[i]]]=i;
    			L[R[R[i]]]=i;
    			L[i]=L[L[i]];
    			R[i]=R[R[i]];
    		}
    	}
    
    	printf("%lld",max(ans,0LL));
    
    	return 0;
    }
    
  • 相关阅读:
    一个简易的词法分析器
    Wireshark的简单使用
    TCP的三次握手和四次挥手详解
    在WEB显示实时视频流
    linux 下 查看进程的启动时间和运行时间
    mac os x 安装adb
    C++ 和 java 使用 AES CBC 128 加解密
    转:Java中String与byte[]的转换
    对称加密
    Google C++ Style Guide在C++11普及后的变化
  • 原文地址:https://www.cnblogs.com/Midoria7/p/13179286.html
Copyright © 2011-2022 走看看