HDOJ1006 Tick and Tick题解
离散化模拟
现实中的钟表受限于精度,秒针走动是一顿一顿的。虽然题目中的钟表指针是连续走动的,但是由于所求答案只是精确到小数点后三位,所以仍然有可能以离散的指针跳动,然后统计happy time的比率。就看采样精度是否足够。因此有了如下离散化模拟的代码:
#include <cstdio>
#include <cstdlib>
#include <cmath>
#define SECONDS_IN_HALF_DAY (12 * 60 * 60)
// 采样精度相对1秒的倍数
#define SAMPLE_RATIO 1
/*
采样精度的选择:
从1开始,查看是否满足样例的输出,如果不是那么加倍;
重复这个过程,直到遇到第一个满足样例输出的精度;
这时采用二分法,找到最小的满足样例输出的精度。
*/
int main()
{
double minHappyDegree;
double maxHappyDegree; // maxHappyDegree = 360 - minHappyDegree
double secondHandDegree;
double minuteHandDegree;
double hourHandDegree;
int totalTicks = SECONDS_IN_HALF_DAY * SAMPLE_RATIO;
int happyTicks;
while(1)
{
scanf("%lf", &minHappyDegree);
if(minHappyDegree == -1.0)
{
break;
}
happyTicks = 0;
maxHappyDegree = 360.0 - minHappyDegree;
for(int tick = 0; tick < totalTicks; tick++)
{
//secondHandDegree = (double)tick / SAMPLE_RATIO / 60 * 360;
secondHandDegree = (double)tick / SAMPLE_RATIO * 6.0;
secondHandDegree = fmod(secondHandDegree, 360.0);
//minuteHandDegree = (double)tick / SAMPLE_RATIO / 60 / 60 * 360;
minuteHandDegree = (double)tick / SAMPLE_RATIO / 10.0;
minuteHandDegree = fmod(minuteHandDegree, 360.0);
//hourHandDegree = (double)tick / SAMPLE_RATIO / 60 / 60 / 12 * 360;
hourHandDegree = (double)tick / SAMPLE_RATIO / 120.0;
hourHandDegree = fmod(hourHandDegree, 360.0);
double dsm = fabs(secondHandDegree - minuteHandDegree);
double dsh = fabs(secondHandDegree - hourHandDegree);
double dmh = fabs(minuteHandDegree - hourHandDegree);
if(dsm >= minHappyDegree && dsm <= maxHappyDegree &&
dsh >= minHappyDegree && dsh <= maxHappyDegree &&
dmh >= minHappyDegree && dmh <= maxHappyDegree)
{
happyTicks++;
}
}
printf("%.3lf\n", (double)happyTicks / totalTicks * 100.0);
}
return EXIT_SUCCESS;
}
问题在于,虽然这样子可以解出来样例输入数据,但是运行超时!我们不得不寻找其他办法。
连续的时间
如果时间是连续的,那么可以设自变量 t ∈ [0, 60*60*12)
,单位为秒,那么有
- 秒针在时刻t相对12点转过的角度为:
ωs(t) = 360/60 * t = 6t
; - 分针在时刻t相对12点转过的角度为:
ωm(t) = 360/3600 * t = t/10
; - 时针在时刻t相对12点转过的角度为:
ωh(t) = 360/(3600*12) * t = t/120
;
因为是周期性的运动,所以我们可以以运动最快的秒针运动一圈为考察单位,将[0, 60*60*12)
每隔60划分为720个考察单位。我们的运气很好,在这样子的考察单位里,不存在指针跨越12点的情况,这意味着将角度规约化到[0,360)
,它们依然是连续的。
为什么说指针跨越12点会不连续呢?比如有一根针从359°运动到361°,我们将其规约化所造成的结果就是指针从359°运动到360°以及从0°运动到1°,不得不细分为两个区间来考察。所以前面所说的考察单位的选择其实是受不能跨越12点这个条件限制的。
接下来,在每一个考察区间([t0, t0 + 60)
)里面,我们需要统计happy time的时间,也就是解关于t的不等式组:
t0 < t < t0 + 60
D < |normal(ωs(t)) - normal(ωm(t))| 且 D < 360 - |normal(ωs(t)) - normal(ωm(t))|
D < |normal(ωm(t)) - normal(ωh(t))| 且 D < 360 - |normal(ωm(t)) - normal(ωh(t))|
D < |normal(ωs(t)) - normal(ωh(t))| 且 D < 360 - |normal(ωs(t)) - normal(ωh(t))|
即
t0 < t < t0 + 60
D < |normal(ωs(t)) - normal(ωm(t))| < 360 -D
D < |normal(ωm(t)) - normal(ωh(t))| < 360 -D
D < |normal(ωs(t)) - normal(ωh(t))| < 360 -D
因为normal操作在给定的考察区间里面,本质上就是减去360的整数倍,而这个减数在整个考察区间里面都不会发生变化,所以像 normal(ωs(t)) - normal(ωm(t))
这样子的式子化简之后就是 k*t + b
的形式。
接下来就是解含有绝对值符号的不等式的问题了,对 k*t + b
分正负情况讨论即可消去绝对值符号。
综上,将每个考察区间的子happy time累加,除以总时间再乘以100即得happy time所占百分比了。
附代码如下:
package pid1006;
import java.math.RoundingMode;
import java.text.DecimalFormat;
public class Main {
public static final int MAX_T = 60 * 60 * 12;
static java.util.Scanner stdin = new java.util.Scanner(System.in);
static java.io.PrintWriter stdout = new java.io.PrintWriter(System.out);
public static void main(String[] args) {
DecimalFormat format = new DecimalFormat("0.000");
format.setRoundingMode(RoundingMode.HALF_UP);
while (true) {
String line = stdin.nextLine();
if (line.equals("-1")) {
break;
}
Double degree = new Double(line);
double percent = solve(degree);
String result = format.format(percent);
stdout.println(result);
}
stdout.flush();
}
/**
* 解关于t的不等式: a < |k * t + b| < c
*
* @param a
* @param k
* 不为0
* @param b
* @param c
* @param result
* 返回解区间
*/
private static void solveInequation(double a, double k, double b, double c,
double mint, double maxt, Interval[] result) {
// 当 k*t + b >= 0时
// 有 a < k*t + b < c
// 即 (a-b)/k < t < (c-b)/k
Interval intervalA = new Interval(-b / k, maxt);
Interval intervalB = new Interval((a - b) / k, (c - b) / k);
Interval intervalC = Interval.intersect(intervalA, intervalB);
// 当 k*t + b < 0 时
// 有 -c < k * t + b < -a
// 即 (-c-b)/k < t < (-a-b)/k
Interval intervalD = new Interval(mint, -b / k);
Interval intervalE = new Interval((-c - b) / k, (-a - b) / k);
Interval intervalF = Interval.intersect(intervalD, intervalE);
result[0] = intervalC;
result[1] = intervalF;
}
private static double solve(Double degree) {
Interval[] arr_sm = new Interval[2];
Interval[] arr_mh = new Interval[2];
Interval[] arr_sh = new Interval[2];
double length = 0.0;
for (int t = 0; t < MAX_T; t += 60) {
int omega_s_normalizer = normalizer(t, 6, 1);
int omega_m_normalizer = normalizer(t, 1, 10);
int omega_h_normalizer = normalizer(t, 1, 120);
solveInequation(degree, 59.0 / 10.0, -omega_s_normalizer
+ omega_m_normalizer, 360 - degree, t, t + 60, arr_sm);
solveInequation(degree, 11.0 / 120.0, -omega_m_normalizer
+ omega_h_normalizer, 360 - degree, t, t + 60, arr_mh);
solveInequation(degree, 719.0 / 120.0, -omega_s_normalizer
+ omega_h_normalizer, 360 - degree, t, t + 60, arr_sh);
for (int i = 0; i < 2; i++) {
Interval ism = arr_sm[i];
for (int j = 0; j < 2; j++) {
Interval jmh = arr_mh[j];
Interval intersect = Interval.intersect(ism, jmh);
for (int k = 0; k < 2; k++) {
Interval ksh = arr_sh[k];
Interval tmp = Interval.intersect(intersect, ksh);
length += (tmp == null) ? 0.0 : tmp.length();
}
}
}
}
return length / MAX_T * 100.0;
}
/**
* 计算角度规约化所需减去的度数
*
* @param t
* 时间
* @param numerator
* 系数的分子
* @param denominator
* 系数的分母
* @return 360 的整数倍
*/
private static int normalizer(int t, int numerator, int denominator) {
numerator = t * numerator;
denominator = denominator * 360;
return (numerator / denominator) * 360;
}
}
/**
* 数学概念上的区间,此处不关心开闭
*
*/
class Interval {
public double left;
public double right;
public Interval(double left, double right) {
this.left = left;
this.right = right;
}
/**
* 求两个区间的交区间
*
* @param a
* @param b
* @return 两个区间的交区间
*/
public static Interval intersect(Interval a, Interval b) {
if (a == null) {
return null;
}
if (b == null) {
return null;
}
if (a.length() <= 0.0 || b.length() <= 0.0) {
return new Interval(0.0, 0.0);
}
double left = a.left > b.left ? a.left : b.left;
double right = a.right < b.right ? a.right : b.right;
if (right - left >= 0) {
return new Interval(left, right);
}
return null;
}
/**
* 求区间长度
*
* @return
*/
public double length() {
return right - left;
}
}