我的思考——最小函数值

点击查看题目来源

Solution

该题目给定了我们一些二次函数,不过这个函数只取了横坐标为正整数部分的值,并且三个系数都为正数,通过代数证明或者图像对称轴分析,都可以肯定,该函数在其定义域(正整数)上,单调递增且恒大于0。
接下来我们再看到题目要求,求这些函数所生成的所有函数值中最小的m个。

暴力求解法

比较暴力的方法是从1开始循环(可能不是最暴力的方法),将1代入所有的函数中,分别得到n个函数值,然后再循环到2,按照这样的方法再来一遍,又有n个函数值,又因为这些都是在其定义域内单调递增的函数,那么首先可以确定1中所有小于等于2中最小函数值的函数值,然后接着按照上述方案做,循环到k时,可以确定从x=1到x=k-1中所有小于等于x=k中最小函数值的函数值,直到确定了m个值。
但是这样的话,思想实在简单,绝配暴力算法一名。暴力之处在于:一、每次求出\(O(n)\)的函数值,耗费\(O(n)\)的空间,最坏情况下要求\(O(mn)\)次,花费\(O(mn)\)空间,而数据一大,时间空间无疑是要超出范围的;二、每次循环求出的函数值得进行排序,如果不排序,那个运算量不敢恭维,假设使用\(\Theta (nlgn)\)复杂度排序,那么也需要花\(\Theta (mnlgn)\)的复杂度;三、再加上每次需要计算x=k中的最小函数值与前面k-1中所有的函数值进行比较,这样在最坏情况下时间代价为:

\[O(\sum _{k=0} ^{m-1} (kn))=O((m-1)mn/2)=O(m^2n) \]

那么,总的算来,就会消耗\(O(O(mn)+\Theta (mnlgn)+O(m^2n))=O(m^2n)\)的时间代价,极其暴力!而且空间上的消耗也是巨大的
那么,我们该如何优化呢?

优化的思想

其实,大家看到函数解析式极其定义域就不难知道,他实际上是给了我们n串排好序的数组,只是每个数组中下标与其值存在一定的对应关系。我们由上面所说的可知,对于每个数组,它们的最小值所在的下标都是1。现在,我们可以想象一下,每个数组都有一个箭头,每个箭头都指向1,然后在所有箭头指向的函数值中,找到最小的那个,此时已经找到了1个最小函数值。接着,刚才输出来的值所对应的箭头就要向后移,指向x=2,然后再去和其他箭头指向的函数值比较,以此类推。下面的两个图形象地展现了一部分操作过程。


那么,现在我们需要将文字描述转化为程序思路。
首先我们需要用三个数组存A、B、C的值,然后需要一个cmin存当前最小值,最后只需要拿一个数组F来表示每个函数中的那个“箭头”所指的位置,那么箭头所指的函数值就会是\(A[k]F[k]^2+B[k]F[k]+C[k]\),至此,思路就很明了了。
下面是我写的程序,很简单,最长耗时测试点用了344ms,没超时。

#include <iostream>
using namespace std;
int main()
{
    int n,m,i,j,cmin,jmin;
    int A[10010], B[10010], C[10010];
    int F[10010];
    cin>>n>>m;
    for(i=0;i<n;i++)
    {
        cin>>A[i]>>B[i]>>C[i];
        F[i]=1;
    }
    for(i=0;i<m;i++)
    {
        cmin=100000000;
        for(j=0;j<n;j++)
        {
            if(A[j]*F[j]*F[j]+B[j]*F[j]+C[j]<cmin)
            {
                cmin=A[j]*F[j]*F[j]+B[j]*F[j]+C[j];
                jmin=j;
            }
        }
        cout<<cmin<<' ';
        F[jmin]++;
    }
    return 0;
}

该程序的时间复杂度为\(\Theta (mn)\)
大家也许会发现,这里每次都重复计算了很多函数的值,浪费了很多时间,那有没有办法针对这一问题进行优化呢?答案是肯定的。

更优化的解法

对于上述问题的优化方法,比较好的是用堆来做。思路是这样的:首先,我们可以在所有“箭头”指向1的时候,对所有箭头对应的函数值建立小根堆;然后,每次从堆顶取走那个数,并将其所对应的“箭头”指向下一个函数值,然后把这个新的函数值代替那个取走的函数值放在堆顶,并自顶向下维护堆(大家可以证明一下,一直这样操作下去,堆的性质恒成立)。下面是我的参考程序:

#include <iostream>
using namespace std;
struct DUI
{
    int val;//箭头表示的函数值
    int x;//每个函数都有被输入进来的先后顺序,这个是第x个输入进来的函数
    //因为堆里面的节点总是在变化的,所以我们要记录哪个函数在哪个位置
} a[10010];
int heap_size;//堆的大小
void CHANGE(int m, int n)//自己写的交换函数
{
    int t;
    t=a[m].val;
    a[m].val=a[n].val;
    a[n].val=t;
    t=a[m].x;
    a[m].x=a[n].x;
    a[n].x=t;
}
void MIN_HEAPIFY(int i)
{
    int l=i*2;//右子节点
    int r=i*2+1;//左子节点
    int smallest;//记录父子节点值最小的那个
    if(l<=heap_size&&a[l].val<a[i].val)
        smallest=l;
    else
        smallest=i;
    if(r<=heap_size&&a[r].val<a[smallest].val)
        smallest=r;//父子节点中值最小的位置
    if(smallest!=i)//父节点最大则不变
    {
        CHANGE(i,smallest);//子节点大则交换父子节点
        MIN_HEAPIFY(smallest);//交换后继续往下维护
    }
}
void BUILD_HEAP()//建立小根堆
{
    int i;
    for(i=heap_size/2; i>0; i--)
        MIN_HEAPIFY(i);//自底向上建堆
}
int main()
{
    int n,m,i,j;
    int A[10010], B[10010], C[10010];
    int F[10010];//每个函数的"箭头"位置
    cin>>n>>m;
    for(i=1; i<=n; i++)
    {
        cin>>A[i]>>B[i]>>C[i];
        F[i]=1;
        a[i].val=A[i]*F[i]*F[i]+B[i]*F[i]+C[i];
        a[i].x=i;//输入的顺序,第i个被输进来的
    }
    heap_size=n;
    BUILD_HEAP();
    for(i=0; i<m; i++)
    {
        cout<<a[1].val<<' ';//输出最小函数值
        F[a[1].x]++;//它所在的函数中的"箭头"往后移
        a[1].val=A[a[1].x]*F[a[1].x]*F[a[1].x]+B[a[1].x]*F[a[1].x]+C[a[1].x];//"箭头"变则值变
        MIN_HEAPIFY(1);//自顶向下维护堆
    }
    return 0;
}

该程序的时间复杂度为\(\Theta (nlgn)\)\(\Theta (mlgn)\)。程序在洛谷上测试通过了,并且最大耗时的测试点耗时8ms。

尾注

  • 这里涉及到的堆的操作的方法来自《算法导论》。
  • 如果有什么错误可以向本人提出,我会做出及时更正。

写在最后

感谢大家的关注和阅读。
本文章借鉴了少许思路,但总体为本人原创,如需转载,请注明出处。

posted @ 2018-05-12 23:28  孤独·粲泽  阅读(193)  评论(0编辑  收藏  举报