[Wf2014]Metal Processing Plant(金属加工厂) 之这是小少主我用二分答案用得最6的一次(所有数据共1S)

来源:互联网 发布:诛仙数据管理工具 编辑:程序博客网 时间:2024/04/28 21:06

题目

【问题描述】

  定义集合S的价值D(S)为:
  这里写图片描述
  现在给你n个元素,并给出其中任意两个元素之间的d(i,j)值,要你将这些元素划分成两个集合A、B。求min{D(A)+D(B)}。注:d(i,j)=d(j,i)。

【输入格式】

  输入数据的第一行是一个整数n,代表元素个数。
  之后n-1行描述的是d(i,j),这部分里,第i行包含n-i个整数,第i行第j列的整数代表的是d(i,i+j)。

【输出格式】

  输出只有一行,一个整数,代表min{D(A)+D(B)}。

【输入样例】

【样例1】
 5
 4 5 0 2
 1 3 7
 2 0
 4

【样例2】
 7
 1 10 5 5 5 5
 5 10 5 5 5
 100 100 5 5
 10 5 5
 98 99
 3

【输出样例】

【样例1】
 4

【样例2】
 15

【数据范围】

1 ≤ n ≤ 200
0<=d(i,i+j)<=10^9

分析

这道题目我们首先可以做到一个判定性问题,也就是给定A集合的最大元素x1,和B集合的最大元素x2,我们可以判定我们能否得到这样的两个集合,因此顺理成章的就有了枚举x1然后二分猜x2的情况,这样要跑几分钟(如果动态维护要快一些,可以几秒跑完,但是少主很懒唉)
后来少主发现我们可以用单调指针完成,因为两边都有单调性了,这样就差不多4秒可以跑完,但是少主家的题库每组数据1S,很伤呀
后来少主又尝试了二分、三分,虽然这个并没有单调性,但是可以过很多组数据,(加起来可以过完的哦,但是少主是不愿意这么做的)

在将少主这个很牛X的方法之前,我们先前通过枚举+二分猜得到了一个很有力的工具,也就是一旦确定了x1,我们就可以确定x2的最小值,那么少主就想能不能也让x1跳得快一点呢,由此便有了以下算法
步骤(注意,所有的A,B都是建立在两个集合的可行解上面的,也就是所有出现过的值,还要手动加上0,也是作为二分答案的一个很好的剪枝,并且对应答案也一定是可行解)
1、确定边界A=1,并且通过二分确定边界最小B,记录一次答案
2、让B减1,确定出对应的A的最小值(肯定不比原来小,而且是对应B的最小可行解),然后再确定新的B(肯定比原来小,答案比确定新的B值之前肯定好些),记录下此时A、B对应的答案。
3、重复步骤2,直到B=1,但是为了不重复,可以加上剪枝,也就是A>=B的时候直接退出即可

这个算法并不是真正意义上的二分,只是利用了二分,实际上应该算是大步跳跃的一种,从步骤2我们大概可以看出一次更新AB之后,新的值肯定比与旧的AB之间的取值要好些,因此我们就可以大概确定我们得到了所有不错的解取一个最优值

时间复杂度非常玄学,经过实验快的可以只有几次更新,慢的是log的几倍,总的来说比较快,可以看做log吧。

希望有路过的大佬神犇能够之处正确性证明的错误或者正确性(我觉得有点牵强好像,但我对了几千组拍没问题,还过了刁钻的测试数据),或者说能够点明一下时间复杂度的奥秘,在下感激不尽(^~^)

代码

#include<cstdio>#include<cstring>#include<iostream>#include<algorithm>using namespace std;const int maxn=500;struct edge{    int to,next;}E[maxn*maxn];int np,first[maxn];void add(int u,int v){    E[++np]=(edge){v,first[u]};    first[u]=np;}int n;int d[maxn][maxn];bool vis[maxn];int a[maxn*maxn],cnt;int other[maxn];int stk[maxn],top;void Init(){    scanf("%d",&n);    a[++cnt]=0;    for(int i=1;i<=n;i++)    {        other[i]=i+n;        other[i+n]=i;        for(int j=i+1;j<=n;j++)        {            scanf("%d",&d[i][j]);            a[++cnt]=d[i][j];        }    }    sort(a+1,a+cnt+1);    cnt=unique(a+1,a+cnt+1)-a-1;}bool DFS(int i){    if(vis[i])return 1;    if(vis[other[i]])return 0;    vis[i]=1;    stk[++top]=i;    for(int p=first[i];p;p=E[p].next)        if(!DFS(E[p].to))            return 0;    return 1;}bool calc(int x,int mid)//i代表A,other代表B,x是a,other是b {    np=0;    memset(first,0,sizeof(first));    for(int i=1;i<=n;i++)    {        for(int j=i+1;j<=n;j++)        {            if(d[i][j]>x)            {                add(i,other[j]);                add(j,other[i]);            }            if(d[i][j]>mid)            {                add(other[i],j);                add(other[j],i);            }        }    }    memset(vis,0,sizeof(vis));    for(int i=1;i<=n;i++)    {        if(vis[i]||vis[other[i]])continue;        top=0;        if(!DFS(i))        {            while(top)                vis[stk[top--]]=0;            if(!DFS(other[i]))                return 0;        }    }    return 1;}int work(int x){    int A=1,B=cnt,ret,mid;    while(A<=B)    {        mid=(A+B)>>1;        if(calc(a[x],a[mid]))            ret=mid,B=mid-1;        else             A=mid+1;    }    return ret;}void solve(){    int A=1,B=work(A);    int ans=a[A]+a[B];    while(A<B)    {        A=work(--B);        B=work(A);        ans=min(ans,a[A]+a[B]);    }    printf("%d\n",ans);}int main(){    Init();    solve();    return 0;}   

注意:(为了刁钻的数据和实现代码的标准化)
1、由于答案可能就是最初的两端,那么初始答案应该直接使用两端作为结果
2、由于可能A和B相互推导是原来的那个数,A=B会进入死循环,因此更新答案范围的时候要求A < B才继续循环,而如果出现了A=B的情况,答案会统计到的

附录

问:为什么这里用2-SAT而不是二分图呢?
答:因为这里用了两个参数限制集合,也就是说集合A和集合B是不同的集合,然而二分图是不能区分集合的。