题目地址: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精度不够,也不用纠结一些奇怪的写法
而且感觉就应该是不少题目希望你做的吧……