hdu 6070 Dirt Ratio(线段树+二分答案)

Dirt Ratio
Time Limit: 18000/9000 MS (Java/Others) Memory Limit: 524288/524288 K (Java/Others)
Total Submission(s): 1835 Accepted Submission(s): 842
Special Judge

Problem Description
In ACM/ICPC contest, the ”Dirt Ratio” of a team is calculated in the following way. First let’s ignore all the problems the team didn’t pass, assume the team passed Xproblems during the contest, and submitted Ytimes for these problems, then the ”Dirt Ratio” is measured as X/Y
. If the ”Dirt Ratio” of a team is too low, the team tends to cause more penalty, which is not a good performance.

Little Q is a coach, he is now staring at the submission list of a team. You can assume all the problems occurred in the list was solved by the team during the contest. Little Q calculated the team’s low ”Dirt Ratio”, felt very angry. He wants to have a talk with them. To make the problem more serious, he wants to choose a continuous subsequence of the list, and then calculate the ”Dirt Ratio” just based on that subsequence.

Please write a program to find such subsequence having the lowest ”Dirt Ratio”.

Input
The first line of the input contains an integer
T(1≤T≤15), denoting the number of test cases.

In each test case, there is an integer
n(1≤n≤60000)in the first line, denoting the length of the submission list.
In the next line, there are npositive integers a1,a2,…,an(1≤ai≤n), denoting the problem ID of each submission.

Output
For each test case, print a single line containing a floating number, denoting the lowest ”Dirt Ratio”. The answer must be printed with an absolute error not greater than 10^−4.

Sample Input
1
5
1 2 1 2 3

Sample Output
0.5000000000
Hint
For every problem, you can assume its final submission is accepted.

题意:
找连续子序列,使(序列中不同的数的个数)/(序列长度)最小

解析:
首先看到网上题解说是二分答案。
二分答案关键操作就是验证答案。
而若答案存在满足的条件一定是 ((size(l.r))/r-l+1)<=mid
size(l,r)表示[l,r]内不同数的个数,而因为式子左边那样计算会有精度问题,所以就把式子化简移项一下
size(l,r)+mid*l<=mid*(r+1)
这样这要找到一个子区间的值满足这个条件,答案就满足

这样之后大神们就想到了线段树,不过在这之前,还要理解因为区间是两个变量(l,r),所以我们就要定一个下来。这里就将l定下来,去遍历区间右端点。正因为如此,式子左边出来num[l,r]是变量外,mid*l就是常量,这样就将size(l,r)+mid*l作为线段树节点的值,同过不断遍历区间右端点来更新num[l,r],并且来查询当前线段数到r为止的所有区间的最小值能不能满足式子

在这里有一个pre[i]数组,表示在a中第i个数在上一次出现的位置,这样每向右遍历一步时,只需要将[pre[i]+1,r]的区间+1

最后这个精度要是10^-4,所以答案二分时要大于10^-4,因此(1-0)/2^n<10^-4 –> n>=14
所以答案最多二分14次

/******补分割线******************/
这道题关键就是里面的check()函数,线段树的叶子结点存的都是左边界L的值,这样我们只需要遍历右区间.
每一次遍历(相当于右区间确定了)其实就是计算了[L‘,R] (L’=[1,R])这区间里面的size(l,r)+mid*l最小值
并且我们只要保证E[L,R],使得 size(l,r)+mid*l就可以了
时间复杂度有原来每一次遍历答案的暴力复杂度O(N^2*logn)降低到O(nlogn*logn)
这里的一个小技巧就是计算size(l,r)的pre[i]数组。每当一个数i出现时,我们只需要更新[他上一次出现的下标+1,这一次的下标]的左区间结点就可以了,因为在这区间的点计算size(l,r)是都是要用到这个i的,而之前的点已经加过了。

#include
#include
#include
#define INF 1999999999
using namespace std;

const int MAXN = 60010;


int n;
int a[MAXN],pre[MAXN],pos[MAXN];
int mark[4*MAXN];
double b[MAXN],Btree[4*MAXN];

void build(double stu[],int l,int r,int root)  //l,r表示他们在stu中的下标,root表示他们在线段树中的坐标
{
    if(l>r)return;
    mark[root]=0;
    if(l==r)
    {
        Btree[root]=stu[l];
        return;
    }

    int mid=(l+r)/2;

    build(stu,l,mid,root*2);
    build(stu,mid+1,r,root*2+1);

    Btree[root]=min(Btree[root*2],Btree[root*2+1]);
}

void pushDown(int root)
{
    if(mark[root]!=0)
    {
        mark[2*root]+=mark[root];
        mark[2*root+1]+=mark[root];

        Btree[2*root]+=mark[root];
        Btree[2*root+1]+=mark[root];

        mark[root]=0;
    }
}

void update(int root,int s1,int e1,int s2,int e2)   //s1,e1表示当前区间,s2,e2表示目标区间
{
    if(e1e2)
        return;
    if(s1>e1)return;
    if(s1>=s2&&e1<=e2)
    {
        mark[root]+=1;
        Btree[root]+=1;
        return;
    }
    pushDown(root);
    int mid=(s1+e1)/2;

    update(root*2,s1,mid,s2,e2);
    update(root*2+1,mid+1,e1,s2,e2);

    Btree[root]=min(Btree[root*2],Btree[root*2+1]);

}

double query(int root,int s1,int e1,int s2,int e2)
{
    if(e1e2)
        return INF;
    if(s1>e1)return INF;
    if(s1>=s2&&e1<=e2)
    {
        return Btree[root];
    }

    pushDown(root);
    int mid=(s1+e1)/2;

    return min(query(root*2,s1,mid,s2,e2),query(root*2+1,mid+1,e1,s2,e2));
}


bool check(double mid)
{
    for(int i=1;i<=n;i++) b[i]=i*mid;   //将每一个线段树根节点的值num[l,r]+mid*l暂存在b中

    build(b,1,n,1);

    for(int i=1;i<=n;i++)   //遍历区间右端点
    {
        update(1,1,n,pre[i]+1,i);  //将(pre[i]+1,i)这一段区间的num[l,r]+1,(因为上一次这个数出现是在pre[i],所以在(pre[i]+1,i)这一段区间内这个数没有出现过,因此要+1,这段区间不同数的个数)
                                      //不断更新num[pre[i]+1,i]的值
        double tmp=query(1,1,n,1,i);   //左区间都是1,遍历右区间i,因为要跟num[l,r]+mid*l<=mid*(r+1)中的r对应,所以只在(一,r)中找
        if(tmp<=mid*(i+1)) return true;  //如果找得到这一样一个区间满足式子,那么成功

    }
    return false;

}

int main()
{
    int t;
    scanf("%d",&t);
    while(t--)
    {
        scanf("%d",&n);
        memset(pos,0,sizeof(pos));
        for(int i=1;i<=n;i++)
        {
            scanf("%d",&a[i]);
            pre[i]=pos[a[i]];   //预处理出每一个i的前一个与a[i]相同的数的出现位置
            pos[a[i]]=i;     //pos[a[i]]表示上一次a[i]的位置
        }

        double l,r;
        l=0;
        r=1;
        for(int i=0;i<14;i++)
        {
            double mid = (l+r)/2;
            if(check(mid)) r=mid;
            else l=mid;
        }
        printf("%.4f\n",l);
    }
    return 0;
}

你可能感兴趣的:(线段树,2017多校)