zoukankan      html  css  js  c++  java
  • [JZOJ]1293.气象牛[区间DP]

    Description

    为了研究农场的气候,Betsy帮助农夫John做了N(1 <= N <= 100)次气压测量并按顺序记录了结果M_1…M_N(1 <= M_i <= 1,000,000).
      Betsy想找出一部分测量结果来总结整天的气压分布. 她想用K(1 <= K <= N)个数s_j
    (1 <= s_1 < s_2 < … < s_K <= N)来概括所有测量结果. 她想限制如下的误差:
      对于任何测量结果子集,每一个非此子集中的结果都会产生误差.总误差是所有测量结果的误差之和.更明确第说, 对于每一个和所有s_j都不同的i:
      * 如果 i 小于 s_1, 误差是:2 * | M_i - M_(s_1) |
      * 如果i在s_j和s_(j+1)之间,误差是:| 2 * M_i - Sum(s_j, s_(j+1)) |
      注:Sum(x, y) = M_x + M_y; (M_x 和 M_y 之和)
      * 如果i大于s_K,误差为:2 * | M_i - M_(s_K) |
      Besty给了最大允许的误差E (1 <= E <= 1,000,000),找出最小的一部分结果使得误差最多为E.

    Input

    第一行: 两个空格分离的数: N 和 E
    第2…N+1行: 第i+1行包含一次测量记录:M_i

    Output

    第一行: 两个空格分开的数: 最少能达到误差小于等于E的测量数目和使用那个测量数目能达到的最小误差.

    Solution

    “数据范围决定解题方法。”
    ——伟大的鲁迅先生

    这道题的操作都带有绝对值(abs),因此许多区间方法都无法使用,好在:

    (1 <= N <= 100)
    

    那么暴力求就是了。
    题目大意:求出 k 个点,称之为观测集合,使得误差总和最小。误差分为三个部分:

    最左的点往左产生的误差

    最右的点往右产生的误差

    观测集合最左最右两点间,即观测区间中每个非观测点产生的误差

    对于观测集合中最左边的点往左的误差:

    i设i为观测集合中最左的点,那么误差计算
    Li=j=1i12MiMjL_i = sum^{i-1}_{j=1} 2 *|M_i - M_j|

    同理,对于最右边的点往右的点:

    Ri=j=1i12MiMjR_i = sum^{i-1}_{j=1} 2 *|M_i - M_j|

    而中间的sum,则为相邻的两个观测点间所有点的误差和:

    i=l+1r12MiSum(l,r)sum^{r-1}_{i=l+1}{| 2 * M_i - Sum(l, r) | } //l r为相邻两个点的下标,l < r
    Sum(l,r)其中Sum(l,r):
    Sum(l,r)=Ml+MrSum(l,r) = M_l + M_r

    也就是枚举(l,r)中每一个点,计算出这个区间的误差。

    long long calc(int l,int r)
    {
    	//枚举在(l,r) 中的每个点产生的误差得到和,返回结果 
    	long long sum = M[l] + M[r];
    	long long ans = 0;
    	for(int i = l+1;i<r;++i)
    		ans += abs(2*M[i] - sum);
    	return ans;
    }
    

    当然可以预处理出来,但预处理也需要不少时间,想到n<=100的复杂度,直接暴力。
    接下来可以处理前后误差的函数,比如pre(int i)计算i前面所有点产生的误差总和啊……
    这次就干脆用了预处理的写法(这个预处理似乎比较方便)

    	long long psum[110],ssum[110];
    	//计算误差前缀和
    	for(int i = 2;i<=n;++i) 
    		for(int j = 1;j<i;++j)//计算以i为左端点产生的前误差和
    			psum[i] += 2*abs(M[j] - M[i]);
    	for(int i = 1;i<n;++i) 
    		for(int j = i+1;j<=n;++j)//计算以i为右端点产生的后误差和
    			ssum[i] += 2*abs(M[j] - M[i]);
    

    然后我们发现了,对于现有的观测集合,如果在右边加入一个新的观测点,那么产生的观测变化只有:

    ij设 i为之前的最右点,j为新的最右点
    Δx=calc(ij)ssum[i]+ssum[j]Delta x = calc(i,j) - ssum[i] + ssum[j]

    于是我们得到了dp的状态转移方程:

    kkii设k为选择k个观测点,i表示讨论范围在前i个点中
    dp[k][i]=min{dp[k1][j]+calc(i,j)ssum[j]+ssum[i]}dp[k][i] = min{dp[k-1][j] + calc(i,j) - ssum[j] + ssum[i]}
    j[k1,i),i[k,n]j in [k-1,i), i in [k,n]

    特殊的,有:

    dp[1][i]=psum[i]+ssum[i]i[1,n]dp[1][i] = psum[i] + ssum[i] i in [1,n] (((//终于用到了psum这个东西!

    于是这道题就结束了,注意输出当前k中获得的最小解即可。
    码风奇丑,还勿介意>_<:

    #include <cstdio>
    #include <cstring>
    using namespace std;
    void read(long long &r)
    {
    	static char c;r = 0;
    	for(c=getchar();c>'9'||c<'0';c=getchar());
    	for(;c>='0'&&c<='9';r=(r<<1)+(r<<3)+c-48,c=getchar());
    }
    inline long long abs(const long long &a)
    {
    	return a>=0?a:-a;
    }
    inline long long min(const long long &a,const long long &b)
    {
    	return a<b?a:b;
    }
    long long psum[110],ssum[110];
    long long n,e;
    long long M[110];
    long long dp[110][110];
    const int INF = 1<<30;
    long long calc(const int &l,const int &r)
    {
    	//枚举在(l,r) 中的每个点产生的误差得到和,返回结果 
    	long long sum = M[l] + M[r];
    	long long ans = 0;
    	for(int i = l+1;i<r;++i)
    		ans += abs(2*M[i] - sum);
    	return ans;
    }
    void getans()
    {
    	int ans = 0;
    	memset(dp,127,sizeof(dp));
    	//初始化k==1时的情况
    	long long minans = INF;
    	for(int i = 1;i<=n;++i) 
    	{
    		dp[1][i] = ssum[i] + psum[i];
    		minans = min(minans,dp[1][i]);
    	}
    	if(minans <= e)
    	{
    		printf("1 %lld",minans);
    		return;
    	}
    	for(int k = 2;k<=n;++k)
    	{
    		if(ans)	break;//找到答案,不用继续了 
    		//选用k个结果时的情况
    		for(int i = k;i<=n;++i) 
    		{
    			//枚举这个结果选哪一个,从k开始
    			//误差可以暴力计算 
    			//也就是枚举上一个分界点
    			for(int j = k-1;j<=i;++j) 
    			{
    				dp[k][i] = min(dp[k-1][j] + calc(j,i) - ssum[j] + ssum[i],dp[k][i]);
    				if(dp[k][i] <= e)
    					ans = k;
    			}
    		}
    	}
    	//枚举第k层得到最小答案
    	printf("%d ",ans);
    	for(int i = 1;i<=n;++i)
    		minans = min(dp[ans][i],minans);
    	printf("%lld",minans);
    }
    void init()
    {
    	//计算误差前缀和
    	for(int i = 2;i<=n;++i) 
    		for(int j = 1;j<i;++j)//计算以i为左端点产生的前误差和
    			psum[i] += 2*abs(M[j] - M[i]);
    	for(int i = 1;i<n;++i) 
    		for(int j = i+1;j<=n;++j)//计算以i为右端点产生的后误差和
    			ssum[i] += 2*abs(M[j] - M[i]);
    }
    int main()
    {
    	read(n);
    	read(e);
    	for(int i = 1;i<=n;++i)
    		read(M[i]);
    	init();
    	getans();
    	return 0;
    }
    
    
    Love programming and insist on improving myself.
  • 相关阅读:
    我在硅谷时候写的文章
    快速上手ABP
    朝花夕拾
    ABP问题速查表
    被低估的.net(上)
    申请Office 365一年免费的开发者账号攻略(2018年10月份版本)
    office365的开发者训练营,免费,在微软广州举办
    数据恢复顾问(DRA)
    使用RMAN对数据文件进行恢复
    Linux环境下利用句柄恢复Oracle误删除的数据文件
  • 原文地址:https://www.cnblogs.com/Clouder-Blog/p/12146656.html
Copyright © 2011-2022 走看看