zoukankan      html  css  js  c++  java
  • 从恒定状态出发,求解未知状态

    从恒定状态出发,求解未知状态 

    湖南省衡阳市第八中学 邹毅

       许多问题都是拥有一个恒定的状态,从这个状态进行演化、转移,产生性质相同,规模更小的子问题。于是我们以此入手,分析出那个恒定的状态与权值,再看如何进行转移产生另一个子问题的解.

    例如下面这个题:

    中位数问题

    给出一个长度为N的非负整数序列A_i,对于所有1 < = k < = (N + 1) / 2, 输出前1,3,5,…个数的中位数。

    Input

    第1行为一个正整数N,表示了序列长度。 N<=10^5

    第2行包含N个整数A_i (-10^9 < = A_i < = 10^9)

     Output

    如题所示。

     输入数据 1

    7

    5 7 8 2 3 1 9

    输出数据 1

    5

    7

    5

    5

     题解:

    就本题的本质来说,就是求一些数字的中位数。如果只是求一次中位数的话,很简单,快速一下即可,时间复杂度为O(N*Log2N),当然如果你对快排理解得够深刻,可以写到O(N).

    但本题是多次求中位数,而且是前1,3,5,7,9……个数字的中位数,这明显是一个公差为2的等差数列。经过分析可得到

    1:对于一个升序的数列,如果每次给它插入或减少2个数字,中位数所位的位置只会发生一个位置的偏移,或者不变。以减少为例来说,如果减少的2个数字,一个在中位数位置的左边,一个在右边,则中位数位置不变;如果2个均在中位数位置的左边,则中位数右移一位,反之则左移一位。

    2:由于本题是事先就给出所有的数字,属于离线询问,所以对于原数列,经过一次快排,我们可根据中位数的定义知道其位置,进而知道其权值。

    3:根据上一条所推出来的恒定的量,我们分析下接下来要解决的问题:求前5个数字的中位数。很明显,我们只需要从排序的数列中去掉最后2个数字即可,产生的后果如第1条所分析。当然此时我们应能做到快速找到去掉的这2个数字在升序数列中的位置,根据其位置与中位数位置的关系进行相应的处理。

    4:得到相应结果,我们必须从升序数列中去掉刚才那2个数字,不难发现此题需要不断的删除元素,于是采用链表结构进行处理。

    5:不断执行上叙步骤,得到所有的解,然后进行输出即可

    代码如下:

    #include<bits/stdc++.h>
    using namespace std;
    const int N=1e5+7;
    int ans[N],pos[N],tot;
    struct node
    {
    	int x,id;
    }a[N];
    struct list 
    {
    	int l,r,v;
    }l[N];
    void insert(int p,int v)
    {
        tot++,l[tot].v=v;
        l[tot].l=p,l[tot].r=l[p].r;
        l[l[p].r].l=tot,l[p].r=tot;
    }
    void erase(int p)
    {
        l[l[p].l].r=l[p].r;
        l[l[p].r].l=l[p].l;
    }
    bool cmp(node x,node y)
    {
    	if(x.x==y.x)
    	return x.id<y.id;
    	return x.x<y.x;
    }
    int main()
    {
    	int n,now,flag=0;
    	scanf("%d",&n);now=n/2+1;
    	for(int i=1;i<=n;i++)
    	scanf("%d",&a[i].x),a[i].id=i;
    	ans[1]=a[1].x;
    	sort(a+1,a+n+1,cmp);
    	for(int i=1;i<=n;i++)
    	{
    		insert(tot,a[i].x);
            pos[a[i].id]=tot;
    	}
        for(int i=n;i>=1;i--)
        {
        	if(i%2==1)
    		{
                if(flag>0)
                    now=l[now].r;
                if(flag<0)
                    now=l[now].l;
                flag=0;
                ans[i]=l[now].v;
            }
            if(pos[i]<now)
                flag++;
    		if(pos[i]>now)
                flag--;
            erase(pos[i]);
    	}
    	for(int i=1;i<=n;i+=2)
    	printf("%d
    ",ans[i]);
    	return 0;
    }
    

      

    例题2:Ksum

    给你一个长度为n的一个正整数数组,于是这个数列有n(n+1)/2个子段 现在求出了这n(n+1)/2个子段之和,并降序排序,请问前K个数是多少。

     Input

    第一行包含两个整数 n 和 k。 接下来一行包含 n 个正整数,代表数组。 ai≤10^9 k≤n(n+1)/2, n≤100000,k≤100000

     Output

    输出 k 个数,代表降序之后的前 k 个数,用空格隔开

     输入数据 1

    3 4

    1 3 4

    输出数据 1

    8 7 4 4

     通过分析不难得出

    1:整个数列的最大值明显为所有数字之和,即数字区间【1..N】

    2:现在希望得到权值第2大的区间和,明显应该将区间【1..N】的左端点1右移1位,或右端点N左移1位,产生于两个子区间【1..N-1】和【2..N】。但它们两个的权值,哪一个更大一些呢?没事,丢到堆中即可。

    3:接着对衍生出来的两个子区间【1..N-1】和【2..N】继续衍生,会发现区间【2..N-1】会被衍生出来2次。我们可以强行去掉其中的一个,保留另一个。于是规定对于某个区间

    【L,R】只有当R=N时,才允许其左边界向右移动。最终可得到以下衍生图:

    代码如下:

    #include<cstdio>
    
    #include<iostream>
    
    #include<algorithm>
    
    #include<queue>
    
    using namespace std;
    
    struct node {
    
           int l,r;
    
           long long s;
    
    } t,z;
    
    int n,k,a[100010];
    
    long long s;
    
    priority_queue<node> q;
    
    bool operator <(node a,node b) {
    
           return a.s<b.s;
    
    }
    
    int main() {
    
           scanf("%d%d",&n,&k);
    
           for(int i=1; i<=n; i++)
    
           {
    
                  scanf("%d",&a[i]);
    
                  s+=a[i];
    
           }
    
           t.s=s;
    
           t.l=1;
    
           t.r=n;
    
           q.push(t);
    
           for(int i=1; i<=k; i++)
    
           {
    
                  t=q.top();
    
                  q.pop();
    
                  printf("%lld ",t.s);
    
                  z.l=t.l;
    
                  z.r=t.r-1;
    
                  z.s=t.s-a[t.r];
    
                  q.push(z);
    
                  if(t.r==n) {
    
                         z.l=t.l+1;
    
                         z.r=t.r;
    
                         z.s=t.s-a[t.l];
    
                         q.push(z);
    
                  }
    
           }
    
           return 0;
    
    }
    
     
    

      

    邻值查找 

    给定一个长度为 n 的序列 A,A 中的数各不相同。对于 A 中的每一个数 Ai,求:

    min|Ai−Aj|,其中1≤j<i

    以及令上式取到最小值的 j(记为 Pi)。若最小值点不唯一,则选择使 Aj 较小的那个。

    输入格式

    第一行输入整数n,代表序列长度。

    第二行输入n个整数A1…An,代表序列的具体数值,数值之间用空格隔开。

    输出格式

    输出共n-1行,每行输出两个整数,数值之间用空格隔开。

    分别表示当i取2~n时,对应的min|Ai−Aj|和Pi的值。

    数据范围

    n≤10^5,|Ai|≤10^9

    输入样例:

    3

    1 5 3

    输出样例:

    4 1 //对于5来说,在它左边并与其差的绝对值最小的是1,差值为4

    2 1 //对于3来说,在它左边并与其差的绝对值最小的是5,差值为2

    题解:

    对于本题最简单的做法就是暴力枚举每个数字i,再枚举它左边的数字即1到i-1.

    再按照题意进行模拟即可,但时间复杂度很明显为O(N^2),对于这种做法,它保证的位置关系这个约束条件。

    如果我们换个角度来进行考虑,先来考虑下差值最小这个约束条件。

    很明显,对于一个数列,将其进行排序后,则对数列中的数字,与之差值最小的数字一定是它的前驱与后继。

    当然数列中第一个数字只有后继,没有前驱。数列中最后一个数字只有前驱,没有后继。

    所以我们可以将数列升序排列出来。

    例如:对于输入的数列3 2 9 5 4

    排序后得到2 3 4 5 9

    按下来我们来确保下位置这个约束条件,于是找到输入数列中的最后一个数字4

    它出现在数列2 3 4 5 9中的第3个位置。此时它左边的3,右边的5均是在原数列中位置编号小于它的,符合题意要求。

    于是差值也就求出来了。接下来,我们从数列2 3 4 5 9中删去4,得到2 3 5 9。再来处理原数列中输入的倒数第二个数字5.............

    代码如下:

    #include<bits/stdc++.h>
    using namespace std;
    const int N=1e5+2;
    int n,i,lx,rx,l[N],r[N],p[N];
    struct str
    {
    	int x,i;
    }
    a[N],ans[N];
    bool cmp(str x,str y)
    {
    	return x.x<y.x;
    }
    int main()
    {
    	scanf("%d",&n);
    	for(i=1;i<=n;i++)
    		scanf("%d",&a[i].x),a[i].i=i;
    	a[0].x=-3e9;
    	a[n+1].x=3e9;
    	sort(a+1,a+n+1,cmp);
    	for(i=1;i<=n;i++)
    		l[i]=i-1,r[i]=i+1,p[a[i].i]=i;
    	for(i=n;i;i--)
    	{
    		lx=abs(a[l[p[i]]].x-a[p[i]].x);
    		rx=abs(a[r[p[i]]].x-a[p[i]].x);
    		if(lx<=rx)
    			ans[i]=(str){lx,a[l[p[i]]].i};
    		else
    			ans[i]=(str){rx,a[r[p[i]]].i};
    		l[r[p[i]]]=l[p[i]];
    		r[l[p[i]]]=r[p[i]];
    	}
    	for(i=2;i<=n;i++)
    		printf("%d %d
    ",ans[i].x,ans[i].i);
    	return 0;
    }
    

      

    上述诸题均还有其它方法进行求解,在此不再赘述,但本文所担的思维方式,在计算机许多算法中都有体现,例如dijkstra求最短路,背包算法等等。

    算法的其实就是在不断的从一个开始状态出发,进行状态的转移,只是这个转移过程,我们可以通过许多方法与手段让它更快一些罢了。。

  • 相关阅读:
    powerful number 小记
    CF573E Bear and Bowling
    Diary 2.0
    【LOJ2540】「PKUWC2018」随机算法
    【Luogu2496】【BZOJ3005】[SDOI2012]体育课
    CF-diary
    【CF1217F】Forced Online Queries Problem
    NOI2019 选做
    Codeforces Round #568 (Div. 2) 选做
    【LOJ2513】「BJOI2018」治疗之雨
  • 原文地址:https://www.cnblogs.com/cutemush/p/15477794.html
Copyright © 2011-2022 走看看