[CodeForces] Looksery Cup 2015 H Degenerate Matrix 与二分时的精度控制

题目地址:http://codeforces.com/contest/549/problem/H

一句话题意:

给定一个2*2矩阵A,要求一个2*2矩阵B,其矩阵行列式det(B)=0,且||A-B||max最小(||A||max表示A的极大范数,就是A里所有元素绝对值的最大值),

请给出最小的||A-B||max


思路:

B的对角线相乘相等就行了吧?

如果A原来对角线相乘就相等,那B直接和A一样就好了

不一样呢?


不放设我们现在允许每个值偏离最多x

主对角线上[a-x,a+x]*[d-x,d+x] ,得到了一个区间

副对角线上[b-x,b+x]*[c-x,c+x] ,得到了另一个区间

只要主对角线上和副对角线上这两个取值区间重叠,那x可以满足要求(我们可以找到一种办法,使得通过加或减x以内的数,让det(B)==0),

否则x太小了,要扩大一点

——于是二分。


到目前为止还好,很正常啊

输出要求乍一看吓了一跳:

Your answer is considered to be correct if its absolute or relative error does not exceed 10 - 9.

你的答案的绝对误差或相对误差不能超过1e-9.


第一感觉,误差不超过1e-9?

那二分的时候左右界差距不超过1e-9就行了,于是愉快的写下了:

while(r-l>1e-9)

作为二分停止条件

然后终测的时候,华丽丽的TLE 38

Why?

第38组是:

43469186 94408326
78066381 -19616812

标准结果是41883387.4306073852

好长的结果!精确数18位!

double能表示的精确值也就只有17~18位吧?

double直接精确到1e-10,这里还真做不到。

怎么办?

(相信我,重点不是思路1,是思路2)


思路1:使用精度更高的类型

随便想想,抓出来3个:

C++ long double

C# Decimal

Java BigDecimal

Java的BigDecimal写起来比较麻烦,这里不做介绍(其实就是懒得写吧)

1)C++ long double

long double可是80位数据类型,精度加大了一些,应该够了吧

把所有中间运算的double改成long double,然后输出写:

printf("%.10Lf\n",ans);

在Linux下不清楚怎么样,在Windows下输出来好多0.00000000000

什么情况?

因为Win下MinGW的标准输入输出默认依赖msvcrt.dll,msvcrt.dll里没考虑过long double的事情……

(参考:http://stackoverflow.com/questions/4089174/printf-and-long-double )

怎么办?在上面的链接中也给出了解答:

加一句#define printf __mingw_printf

把输出printf换成MinGW的printf,这样就行了。


#include <stdio.h>
#include <ctype.h>
#include <string.h>
#include <stdlib.h>
#include <limits.h>
#include <math.h>
#include <algorithm>
using namespace std;
#define printf __mingw_printf

const int di[8][2]={{-1,-1},{-1,1},{1,-1},{1,1},{-1,0},{1,0},{0,-1},{0,1}};

int main()
{
    double a,b,c,d;
    scanf("%lf%lf%lf%lf",&a,&b,&c,&d);
    if(fabs(a*d-b*c)<1e-10){
        puts("0.0000000000");return 0 ;
    }
    if(a*d>b*c){
        swap(a,b);
        swap(c,d);
    }
    long double l=0,r=1e10,mid;
    while(r-l>1e-10){
        mid=l/2.0+r/2.0;
        long double l1=a*d,r1=a*d,l2=b*c,r2=b*c;
        for(int i=0;i<8;i++){
            l1=min(l1,(a+di[i][0]*mid)*(d+di[i][1]*mid));
            l2=min(l2,(b+di[i][0]*mid)*(c+di[i][1]*mid));
            r1=max(r1,(a+di[i][0]*mid)*(d+di[i][1]*mid));
            r2=max(r2,(b+di[i][0]*mid)*(c+di[i][1]*mid));
        }
        if(r1<l2||r2<l1)
            l=mid;
        else r=mid;
    }
    printf("%.10Lf\n",(l+r)/2.0);
    return 0;
}

2) C# Decimal

Decimal类型的具体说明参见 https://msdn.microsoft.com/zh-cn/library/364x0z75.aspx

大致范围是(-7.9 x 1028 - 7.9 x 1028) / (100 - 28)

精度挺可怕的,28~29个有效位(人家是128位定点数,不是吃白饭的)

那就用Decimal试一发

(注意!!!C#里的Decimal和double没法直接隐式转换的,你必须要强制类型转换,但是int等整型和Decimal在一起直接运算的时候没关系,

还有如果有浮点数常数要赋值给Decimal,请记得在最后加上M,大写的M,来告诉编译器这是个Decimal常数)

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.IO;
using System.Numerics;
using E = System.Linq.Enumerable;

namespace CodeForces549H {
    class Program {
        protected IOHelper io;

        public Program(string inputFile, string outputFile) {
            io = new IOHelper(inputFile, outputFile, Encoding.Default);

            int[][] di = new int[][] { new int[] { -1, -1 }, new int[] { -1, 1 }, new int[] { 1, -1 }, new int[] { 1, 1 }, new int[] { -1, 0 }, new int[] { 1, 0 }, new int[] { 0, -1 }, new int[] { 0, 1 } };
            Decimal a = io.NextDecimal(), b = io.NextDecimal(), c = io.NextDecimal(), d = io.NextDecimal();

            if (a * d > b * c) {
                Decimal tmp = a; a = d; d = tmp;
                tmp = b; b = c; c = tmp;
            }

            Decimal l = 0, r = 1e10M, mid;
            while(r-l>1e-10M){
		        mid=l/2.0M+r/2.0M;
		        Decimal l1=a*d,r1=a*d,l2=b*c,r2=b*c;
		        for(int i=0;i<8;i++){
                    l1 = Math.Min(l1, (a + di[i][0] * mid) * (d + di[i][1] * mid));
                    l2 = Math.Min(l2, (b + di[i][0] * mid) * (c + di[i][1] * mid));
                    r1 = Math.Max(r1, (a + di[i][0] * mid) * (d + di[i][1] * mid));
                    r2 = Math.Max(r2, (b + di[i][0] * mid) * (c + di[i][1] * mid));
		        }
		        if(r1<l2||r2<l1)
			        l=mid;
		        else r=mid;
	        }
            io.Write((l+r)/2.0M, 10);


            io.Dispose();
        }

        static void Main(string[] args) {
            Program myProgram = new Program(null, null);
        }
    }

    class IOHelper : IDisposable {
        public StreamReader reader;
        public StreamWriter writer;

        public IOHelper(string inputFile, string outputFile, Encoding encoding) {
            if (inputFile == null)
                reader = new StreamReader(Console.OpenStandardInput(), encoding);
            else
                reader = new StreamReader(inputFile, encoding);

            if (outputFile == null)
                writer = new StreamWriter(Console.OpenStandardOutput(), encoding);
            else
                writer = new StreamWriter(outputFile, false, encoding);

            curLine = new string[] { };
            curTokenIdx = 0;
        }

        string[] curLine;
        int curTokenIdx;

        char[] whiteSpaces = new char[] { ' ', '\t', '\r', '\n' };

        public bool hasNext() {
            if (curTokenIdx >= curLine.Length) {
                //Read next line
                string line = reader.ReadLine();
                if (line != null)
                    curLine = line.Split(whiteSpaces, StringSplitOptions.RemoveEmptyEntries);
                else
                    curLine = new string[] { };
                curTokenIdx = 0;
            }

            return curTokenIdx < curLine.Length;
        }

        public string NextToken() {
            return hasNext() ? curLine[curTokenIdx++] : null;
        }

        public int NextInt() {
            return int.Parse(NextToken());
        }

        public double NextDouble() {
            string tkn = NextToken();
            return double.Parse(tkn, System.Globalization.CultureInfo.InvariantCulture);
        }

        public Decimal NextDecimal() {
            string tkn = NextToken();
            return Decimal.Parse(tkn, System.Globalization.CultureInfo.InvariantCulture);
        }

        public void Write(double val, int precision) {
            writer.Write(val.ToString("F" + precision, System.Globalization.CultureInfo.InvariantCulture));
        }
        public void Write(Decimal val, int precision) {
            writer.Write(val.ToString("F" + precision, System.Globalization.CultureInfo.InvariantCulture));
        }

        public void Write(object stringToWrite) {
            writer.Write(stringToWrite);
        }

        public void WriteLine(Decimal val, int precision) {
            writer.WriteLine(val.ToString("F" + precision, System.Globalization.CultureInfo.InvariantCulture));
        }

        public void WriteLine(double val, int precision) {
            writer.WriteLine(val.ToString("F" + precision, System.Globalization.CultureInfo.InvariantCulture));
        }

        public void WriteLine(object stringToWrite) {
            writer.WriteLine(stringToWrite);
        }

        public void Dispose() {
            try {
                if (reader != null) {
                    reader.Dispose();
                }
                if (writer != null) {
                    writer.Flush();
                    writer.Dispose();
                }
            } catch { };
        }


        public void Flush() {
            if (writer != null) {
                writer.Flush();
            }
        }
    }
}

嗯,这样也比较愉快呢~

(C#的小数输出时的小数点位数处理还是建议按我这里的ioHelper里的写法吧,内建方法不消耗脑力还准确)


思路2:利用好相对误差的条件

我们上面折腾了半天,目的只是为了达到绝对误差的要求

如果解再大一些,可能有1e15那么大呢?

注意到,题目输出要求说了,绝对误差或相对误差允许在1e-9范围以内

……妈呀,写二分这么多年,就没用过相对误差……


简单修改一个地方,就是二分的终止条件:

while(r-l>1e-9&&r-l>(l+r)/2.0*1e-9)

相对误差和绝对误差都不满足时,继续二分,只要一个满足就停止二分。

解释一下相对误差的部分r-l>(l+r)/2.0*1e-9

我们假设解应该是二分区间的中点(区间越小越精确了)

然后认为误差应该是二分区间的长度(最糟糕情况)

那么相对误差就是(r-l)/((l+r)/2.0)

相对误差<1e-9的时候才满足条件

(当然你认为解是左端点l也行,但是l有可能为0,那就坑了)

然后就用double过掉了,过掉了,过掉了……

#include <stdio.h>
#include <ctype.h>
#include <string.h>
#include <stdlib.h>
#include <limits.h>
#include <math.h>
#include <algorithm>
using namespace std;

const int di[8][2]={{-1,-1},{-1,1},{1,-1},{1,1},{-1,0},{1,0},{0,-1},{0,1}};

int main()
{
    double a,b,c,d;
    scanf("%lf%lf%lf%lf",&a,&b,&c,&d);
    if(fabs(a*d-b*c)<1e-10){
        puts("0.0000000000");return 0 ;
    }
    if(a*d>b*c){
        swap(a,b);
        swap(c,d);
    }
    double l=0,r=1e10,mid;
    while(r-l>1e-9&&r-l>(l+r)/2.0*1e-9){
        mid=l/2.0+r/2.0;
        double l1=a*d,r1=a*d,l2=b*c,r2=b*c;
        for(int i=0;i<8;i++){
            l1=min(l1,(a+di[i][0]*mid)*(d+di[i][1]*mid));
            l2=min(l2,(b+di[i][0]*mid)*(c+di[i][1]*mid));
            r1=max(r1,(a+di[i][0]*mid)*(d+di[i][1]*mid));
            r2=max(r2,(b+di[i][0]*mid)*(c+di[i][1]*mid));
        }
        if(r1<l2||r2<l1)
            l=mid;
        else r=mid;
    }
    printf("%.10f\n",(l+r)/2.0);
    return 0;
}


=================================================================

我真的是第一次写二分利用相对误差做边界条件,让大家见笑了。

——感觉利用相对误差是开辟了新天地吧,迭代结束早,不怕double精度不够,也不用纠结一些奇怪的写法

而且感觉就应该是不少题目希望你做的吧……

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值