uoj-contest-lib.php 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369
  1. <?php
  2. define("CONTEST_NOT_STARTED", 0);
  3. define("CONTEST_IN_PROGRESS", 1);
  4. define("CONTEST_PENDING_FINAL_TEST", 2);
  5. define("CONTEST_TESTING", 10);
  6. define("CONTEST_FINISHED", 20);
  7. function g2_g($x) {
  8. return 1 / sqrt(1 + 3 * $x * $x / M_PI / M_PI);
  9. }
  10. function g2_E($u, $uj, $pj) {
  11. return 1 / (1 + exp(-g2_g($pj) * ($u - $uj)));
  12. }
  13. function g2_f($x, $delta, $phi, $v, $a, $tau) {
  14. return (exp($x) * ($delta * $delta - $phi * $phi - $v - exp($x))
  15. / (2 * pow($phi * $phi + $v + exp($x), 2)))
  16. - ($x - $a) / $tau / $tau;
  17. }
  18. function calcRatingGlicko2($standings, &$new_rating, &$new_rd, &$new_vol) {
  19. $TAU = 0.6;
  20. $n = count($standings);
  21. $rating = array();
  22. $rd = array();
  23. $vol = array();
  24. for ($i = 0; $i < $n; ++$i) {
  25. $rating[$i] = ($standings[$i][2][1] - 1500) / 173.7178;
  26. $rd[$i] = $standings[$i][2][2] / 173.7178;
  27. $vol[$i] = ($standings[$i][2][3] ?? 0.06);
  28. if ($vol[$i] <= 0) $vol[$i] = 0.06;
  29. }
  30. if ($n == 1) {
  31. $new_rating[0] = $rating[0];
  32. $new_rd[0] = $rd[0];
  33. $new_vol[0] = $vol[0];
  34. }
  35. for ($i = 0; $i < $n; $i++) {
  36. $delta = 0;
  37. $v = 0;
  38. for ($j = 0; $j < $n; $j++) if ($i != $j) {
  39. $s = 0;
  40. if ($standings[$i][3] > $standings[$j][3])
  41. $s = 0;
  42. else if ($standings[$i][3] == $standings[$j][3])
  43. $s = 0.5;
  44. else
  45. $s = 1;
  46. $e = g2_E($rating[$i], $rating[$j], $rd[$j]);
  47. $v += pow(g2_g($rd[$j]), 2) * $e * (1 - $e);
  48. $delta += g2_g($rd[$j]) * ($s - $e);
  49. }
  50. if ($v < 1e-20) $v = 1e-20;
  51. $v = 1 / $v;
  52. $delta *= $v;
  53. if ($delta > 10) $delta = 10;
  54. if ($delta < -10) $delta = -10;
  55. $a = 2 * log($vol[$i]);
  56. $A = $a;
  57. if ($delta * $delta > $rd[$i] * $rd[$i] + $v)
  58. $B = log($delta * $delta - $rd[$i] * $rd[$i] - $v);
  59. else {
  60. for ($k = 1; $k < 100 && g2_f($a - $k * $TAU, $delta, $rd[$i], $v, $a, $TAU) < 0; ++$k)
  61. ;
  62. $B = $a - $k * $TAU;
  63. }
  64. $fa = g2_f($A, $delta, $rd[$i], $v, $a, $TAU);
  65. $fb = g2_f($B, $delta, $rd[$i], $v, $a, $TAU);
  66. while ($fa * $fb > 0) {
  67. $B -= $TAU;
  68. $fb = g2_f($B, $delta, $rd[$i], $v, $a, $TAU);
  69. }
  70. $k = 0;
  71. while (abs($B - $A) > 1e-6 && $k < 100) {
  72. if (!is_finite($fa) || !is_finite($fb)) break;
  73. if (abs($fa - $fb) < 1e-15) break;
  74. $C = $A + ($A - $B) * $fa / ($fb - $fa);
  75. $fc = g2_f($C, $delta, $rd[$i], $v, $a, $TAU);
  76. if ($fc * $fb <= 0) {
  77. $A = $B;
  78. $fa = $fb;
  79. }
  80. else
  81. $fa = $fa / 2;
  82. $B = $C;
  83. $fb = $fc;
  84. ++$k;
  85. }
  86. $new_vol[$i] = exp(($A + $B) / 4);
  87. $new_rd[$i] = sqrt($rd[$i] * $rd[$i] + $new_vol[$i] * $new_vol[$i]);
  88. $new_rd[$i] = 1 / sqrt(1 / $v + 1 / $new_rd[$i] / $new_rd[$i]);
  89. $new_rating[$i] = $rating[$i] + $delta / $v * $new_rd[$i] * $new_rd[$i];
  90. $new_rating[$i] = $new_rating[$i] * 173.7178 + 1500;
  91. $new_rd[$i] *= 173.7178;
  92. }
  93. }
  94. /*
  95. * 新的 Rating 计算函数,采用 Codeforces 算法。
  96. * 具体算法是:
  97. * 设参赛前每人的 Rating 是 r_i, 比赛的最终排名是 R_i,那么
  98. * 首先承认:在先前 Rating 的基础上,i 战胜 j 的概率是 P_ij = \dfrac{1}{1 + 10^{\dfrac{r_j - r_i}{400}}}
  99. * 立即得到 i 的期望排名 E_i = \sum_{j \ne i} P_ji + 1
  100. * 显然,真正的排名和期望排名之间的差距,就是 rating delta 的来源。
  101. * 我们取 S_i = \sqrt{E_i R_i},二分出一个 u 使得计算出的 S_i = E_i,rating 应当向 u 的方向变化。
  102. * 取 \delta = \dfrac{u + r_i}{2},随后我们对 \delta 进行微调。
  103. * 第一次微调:取 d = \dfrac{-1 - \sum_{i} \delta_i}{n},将所有人的 \delta 加上 d。
  104. * 第二次微调:取排名前 s = \min{n, 4\sqrt n} 高的人,让这些人的 rating 变化量之和为零,得到 d。
  105. * 随后再将所有人的 \delta 加上 d,得到最终的 rating。
  106. * */
  107. function calcRatingX($standings, &$new_ratings, &$new_rating_rds, &$new_rating_vols) {
  108. $n = count($standings);
  109. $rating = array();
  110. $delta = array();
  111. $ds = 0;
  112. for ($i = 0; $i < $n; ++$i) {
  113. $rating[$i] = $standings[$i][2][1];
  114. $new_rating_rds[$i] = 0;
  115. $new_rating_vols[$i] = 0;
  116. }
  117. for ($i = 0; $i < $n; ++$i) {
  118. $l = 0;
  119. $r = 4000;
  120. while ($l <= $r) {
  121. $mid = ($l + $r) / 2;
  122. $e = 1;
  123. for ($j = 0; $j < $n; ++$j)
  124. if ($i != $j)
  125. $e += 1 / (1 + pow(10, ($mid - $rating[$j]) / 400));
  126. $s = sqrt($e * ($i + 1));
  127. if ($e <= $s)
  128. $r = $mid - 1;
  129. else
  130. $l = $mid + 1;
  131. }
  132. $delta[$i] = (($l + $r) / 2 - $rating[$i]) / 2;
  133. $ds += $delta[$i];
  134. }
  135. $ds = (-1 - $ds) / $n;
  136. for ($i = 0; $i < $n; ++$i)
  137. $delta[$i] += $ds;
  138. $s = min($n, 4 * sqrt($n));
  139. $ds = 0;
  140. for ($i = 0; $i < $s; ++$i)
  141. $ds += $delta[$i];
  142. $ds = -$ds / $s;
  143. for ($i = 0; $i < $s; ++$i)
  144. $delta[$i] += $ds;
  145. for ($i = 0; $i < $n; ++$i)
  146. $new_ratings[$i] = ceil($rating[$i] + $delta[$i]);
  147. }
  148. function calcRating($standings, &$new_ratings, &$new_rating_rds, &$new_rating_vols, $K = 400) {
  149. $DELTA = 500;
  150. $n = count($standings);
  151. $rating = array();
  152. for ($i = 0; $i < $n; ++$i) {
  153. $rating[$i] = $standings[$i][2][1];
  154. $new_rating_rds[$i] = 0;
  155. $new_rating_vols[$i] = 0;
  156. }
  157. $rank = array();
  158. $foot = array();
  159. for ($i = 0; $i < $n; ) {
  160. $j = $i;
  161. while ($j + 1 < $n && $standings[$j + 1][3] == $standings[$j][3]) {
  162. ++$j;
  163. }
  164. $our_rk = 0.5 * (($i + 1) + ($j + 1));
  165. while ($i <= $j) {
  166. $rank[$i] = $our_rk;
  167. $foot[$i] = $n - $rank[$i];
  168. $i++;
  169. }
  170. }
  171. $weight = array();
  172. for ($i = 0; $i < $n; ++$i) {
  173. $weight[$i] = pow(7, $rating[$i] / $DELTA);
  174. }
  175. $exp = array_fill(0, $n, 0);
  176. for ($i = 0; $i < $n; ++$i) {
  177. for ($j = 0; $j < $n; ++$j) {
  178. if ($j != $i) {
  179. $exp[$i] += $weight[$i] / ($weight[$i] + $weight[$j]);
  180. }
  181. }
  182. }
  183. for ($i = 0; $i < $n; $i++) {
  184. $new_ratings[$i] = $rating[$i];
  185. $new_ratings[$i] += ceil($K * ($foot[$i] - $exp[$i]) / ($n - 1));
  186. }
  187. for ($i = $n - 1; $i >= 0; $i--) {
  188. if ($i + 1 < $n && $standings[$i][3] != $standings[$i + 1][3]) {
  189. break;
  190. }
  191. if ($new_ratings[$i] > $rating[$i]) {
  192. $new_ratings[$i] = $rating[$i];
  193. }
  194. }
  195. for ($i = 0; $i < $n; $i++) {
  196. if ($new_ratings[$i] < 0) {
  197. $new_ratings[$i] = 0;
  198. }
  199. }
  200. }
  201. function genMoreContestInfo(&$contest) {
  202. $contest['start_time_str'] = $contest['start_time'];
  203. $contest['start_time'] = new DateTime($contest['start_time']);
  204. $contest['end_time'] = clone $contest['start_time'];
  205. $contest['end_time']->add(new DateInterval("PT${contest['last_min']}M"));
  206. if ($contest['status'] == 'unfinished') {
  207. if (UOJTime::$time_now < $contest['start_time']) {
  208. $contest['cur_progress'] = CONTEST_NOT_STARTED;
  209. } elseif (UOJTime::$time_now < $contest['end_time']) {
  210. $contest['cur_progress'] = CONTEST_IN_PROGRESS;
  211. } else {
  212. $contest['cur_progress'] = CONTEST_PENDING_FINAL_TEST;
  213. }
  214. } elseif ($contest['status'] == 'testing') {
  215. $contest['cur_progress'] = CONTEST_TESTING;
  216. } elseif ($contest['status'] == 'finished') {
  217. $contest['cur_progress'] = CONTEST_FINISHED;
  218. }
  219. $contest['extra_config'] = json_decode($contest['extra_config'], true);
  220. if (!isset($contest['extra_config']['standings_version'])) {
  221. $contest['extra_config']['standings_version'] = 2;
  222. }
  223. }
  224. function updateContestPlayerNum($contest) {
  225. DB::update("update contests set player_num = (select count(*) from contests_registrants where contest_id = {$contest['id']}) where id = {$contest['id']}");
  226. }
  227. // problems: pos => id
  228. // data : id, submit_time, submitter, problem_pos, score
  229. // people : username, user_rating
  230. function queryContestData($contest, $config = array()) {
  231. mergeConfig($config, [
  232. 'pre_final' => false
  233. ]);
  234. $problems = [];
  235. $prob_pos = [];
  236. $n_problems = 0;
  237. $result = DB::query("select problem_id from contests_problems where contest_id = {$contest['id']} order by problem_id");
  238. while ($row = DB::fetch($result, MYSQLI_NUM)) {
  239. $prob_pos[$problems[] = (int)$row[0]] = $n_problems++;
  240. }
  241. $data = [];
  242. if ($config['pre_final']) {
  243. $result = DB::query("select id, submit_time, submitter, problem_id, result from submissions"
  244. ." where contest_id = {$contest['id']} and score is not null order by id");
  245. while ($row = DB::fetch($result, MYSQLI_NUM)) {
  246. $r = json_decode($row[4], true);
  247. if (!isset($r['final_result'])) {
  248. continue;
  249. }
  250. $row[0] = (int)$row[0];
  251. $row[3] = $prob_pos[$row[3]];
  252. $row[4] = (int)($r['final_result']['score']);
  253. $data[] = $row;
  254. }
  255. } else {
  256. if ($contest['cur_progress'] < CONTEST_FINISHED) {
  257. $result = DB::query("select id, submit_time, submitter, problem_id, score from submissions"
  258. ." where contest_id = {$contest['id']} and score is not null order by id");
  259. } else {
  260. $result = DB::query("select submission_id, date_add('{$contest['start_time_str']}', interval penalty second),"
  261. ." submitter, problem_id, score from contests_submissions where contest_id = {$contest['id']}");
  262. }
  263. while ($row = DB::fetch($result, MYSQLI_NUM)) {
  264. $row[0] = (int)$row[0];
  265. $row[3] = $prob_pos[$row[3]];
  266. $row[4] = (int)$row[4];
  267. $data[] = $row;
  268. }
  269. }
  270. $people = [];
  271. $result = DB::query("select username, user_rating, user_rd, user_vol from contests_registrants where contest_id = {$contest['id']} and has_participated = 1");
  272. while ($row = DB::fetch($result, MYSQLI_NUM)) {
  273. $row[1] = (int)$row[1];
  274. $people[] = $row;
  275. }
  276. return ['problems' => $problems, 'data' => $data, 'people' => $people];
  277. }
  278. function calcStandings($contest, $contest_data, &$score, &$standings, $update_contests_submissions = false) {
  279. // score: username, problem_pos => score, penalty, id
  280. $score = array();
  281. $n_people = count($contest_data['people']);
  282. $n_problems = count($contest_data['problems']);
  283. foreach ($contest_data['people'] as $person) {
  284. $score[$person[0]] = array();
  285. }
  286. foreach ($contest_data['data'] as $submission) {
  287. $penalty = (new DateTime($submission[1]))->getTimestamp() - $contest['start_time']->getTimestamp();
  288. if ($contest['extra_config']['standings_version'] >= 2) {
  289. if ($submission[4] == 0) {
  290. $penalty = 0;
  291. }
  292. }
  293. $score[$submission[2]][$submission[3]] = array($submission[4], $penalty, $submission[0]);
  294. }
  295. // standings: rank => score, penalty, [username, user_rating, user_rd, user_vol], virtual_rank
  296. $standings = array();
  297. foreach ($contest_data['people'] as $person) {
  298. $cur = array(0, 0, $person);
  299. for ($i = 0; $i < $n_problems; $i++) {
  300. if (isset($score[$person[0]][$i])) {
  301. $cur_row = $score[$person[0]][$i];
  302. $cur[0] += $cur_row[0];
  303. $cur[1] += $cur_row[1];
  304. if ($update_contests_submissions) {
  305. DB::insert("insert into contests_submissions (contest_id, submitter, problem_id, submission_id, score, penalty) values ({$contest['id']}, '{$person[0]}', {$contest_data['problems'][$i]}, {$cur_row[2]}, {$cur_row[0]}, {$cur_row[1]})");
  306. }
  307. }
  308. }
  309. $standings[] = $cur;
  310. }
  311. usort($standings, function($lhs, $rhs) {
  312. if ($lhs[0] != $rhs[0]) {
  313. return $rhs[0] - $lhs[0];
  314. } elseif ($lhs[1] != $rhs[1]) {
  315. return $lhs[1] - $rhs[1];
  316. } else {
  317. return strcmp($lhs[2][0], $rhs[2][0]);
  318. }
  319. });
  320. $is_same_rank = function($lhs, $rhs) {
  321. return $lhs[0] == $rhs[0] && $lhs[1] == $rhs[1];
  322. };
  323. for ($i = 0; $i < $n_people; $i++) {
  324. if ($i == 0 || !$is_same_rank($standings[$i - 1], $standings[$i])) {
  325. $standings[$i][] = $i + 1;
  326. } else {
  327. $standings[$i][] = $standings[$i - 1][3];
  328. }
  329. }
  330. }