You can not select more than 25 topics Topics must start with a chinese character,a letter or number, can include dashes ('-') and can be up to 35 characters long.

README.md 18 kB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457
  1. # Algorithm
  2. ---
  3. 天梯分数计算算法
  4. 原始记录在:<https://github.com/eesast/THUAI5/discussions/86>
  5. 内容如下:
  6. ## THUAI4
  7. 关于根据队式每场比赛的分数映射到天梯分数的问题:
  8. 队式比赛为两队对战,每队得分的区间均为 [0, 2500]。
  9. 以 tanh 函数为基础进行设计。
  10. 设计原则如下:
  11. 1. 输的扣少量天梯分,赢的得大量天梯分
  12. 2. 本就有极高天梯分数的虐本就天梯分数低的,这种降维打击现象,天梯分数涨幅极小甚至不涨天梯分
  13. 3. 如果在某场比赛中,两者表现差不多,即赢的比输的得分高得不多的话,那么天梯分数涨幅也不是很高
  14. 4. 如果本来天梯分数很低的,赢了天梯分数很高的,那么他得到的天梯分会较高,而另一个人,天梯分数降分稍多一些
  15. 5. 如果天梯分数低的赢了天梯分数高的,但是这场比赛赢得不多的话,会把两人的分数向中间靠拢
  16. 6. 总体上,赢的队伍不会降天梯分;输的队伍不会加天梯分
  17. 7. 其他条件相同的情况下,在本场游戏中得分越多,加的天梯分数也越高
  18. 上述原则可以保证以下两个目的的达成:
  19. 1. 总体来看,进行的游戏场次越多,所有队伍的平均天梯分数就越高
  20. 2. 经过足够多次的游戏场次,实力有一定差距的队伍的天体分数差距逐渐拉开,实力相近的队伍的天梯分数不会差别过大,各支队伍的排名趋近于收敛
  21. 用 cpp 代码编写算法代码如下(`cal` 函数):
  22. ```cpp
  23. #include <iostream>
  24. #include <algorithm>
  25. #include <cmath>
  26. using namespace std;
  27. template <typename T>
  28. using mypair = pair<T, T>;
  29. // orgScore 是天梯中两队的分数;competitionScore 是这次游戏两队的得分
  30. mypair<int> cal(mypair<int> orgScore, mypair<int> competitionScore)
  31. {
  32. // 调整顺序,让第一个元素成为获胜者,便于计算
  33. bool reverse = false; // 记录是否需要调整
  34. if (competitionScore.first < competitionScore.second)
  35. {
  36. reverse = true;
  37. }
  38. else if (competitionScore.first == competitionScore.second)
  39. {
  40. if (orgScore.first == orgScore.second) // 完全平局,不改变天梯分数
  41. {
  42. return orgScore;
  43. }
  44. if (orgScore.first > orgScore.second) // 本次游戏平局,但一方天梯分数高,另一方天梯分数低,需要将两者向中间略微靠拢,因此天梯分数低的定为获胜者
  45. {
  46. reverse = true;
  47. }
  48. else
  49. {
  50. reverse = false;
  51. }
  52. }
  53. if (reverse) // 如果需要换,换两者的顺序
  54. {
  55. swap(competitionScore.first, competitionScore.second);
  56. swap(orgScore.first, orgScore.second);
  57. }
  58. // 转成浮点数
  59. mypair<double> orgScoreLf;
  60. mypair<double> competitionScoreLf;
  61. orgScoreLf.first = orgScore.first;
  62. orgScoreLf.second = orgScore.second;
  63. competitionScoreLf.first = competitionScore.first;
  64. competitionScoreLf.second = competitionScore.second;
  65. mypair<int> resScore;
  66. const double deltaWeight = 80.0; // 差距悬殊判断参数,比赛分差超过此值就可以认定为非常悬殊了,天梯分数增量很小,防止大佬虐菜鸡的现象造成两极分化
  67. double delta = (orgScoreLf.first - orgScoreLf.second) / deltaWeight;
  68. cout << "Tanh delta: " << tanh(delta) << endl;
  69. {
  70. const double firstnerGet = 8e-5; // 胜利者天梯得分权值
  71. const double secondrGet = 5e-6; // 失败者天梯得分权值
  72. double deltaScore = 100.0; // 两队竞争分差超过多少时就认为非常大
  73. double correctRate = (orgScoreLf.first - orgScoreLf.second) / 100.0; // 订正的幅度,该值越小,则在势均力敌时天梯分数改变越大
  74. double correct = 0.5 * (tanh((competitionScoreLf.first - competitionScoreLf.second - deltaScore) / deltaScore - correctRate) + 1.0); // 一场比赛中,在双方势均力敌时,减小天梯分数的改变量
  75. resScore.first = orgScore.first + round(competitionScoreLf.first * competitionScoreLf.first * firstnerGet * (1 - tanh(delta)) * correct); // 胜者所加天梯分
  76. resScore.second = orgScore.second - round((2500.0 - competitionScoreLf.second) * (2500.0 - competitionScoreLf.second) * secondrGet * (1 - tanh(delta)) * correct); // 败者所扣天梯分,2500 为得分的最大值(THUAI4 每场得分介于 0~2500 之间)
  77. }
  78. // 如果换过,再换回来
  79. if (reverse)
  80. {
  81. swap(resScore.first, resScore.second);
  82. }
  83. return resScore;
  84. }
  85. ```
  86. **特别注意**:此算法是在 THUAI4 的比赛直接得分封顶为 2500 分、最低不低于 0 分的前提下设计的,因此并不一定适用于 THUAI5 的情形。
  87. ## THUAI5
  88. 今年把得分上限这个东西去掉了。理论上今年可以得很高很高分,但是我估计大部分比赛得分在400-600左右,最高估计1000左右。算法 借 鉴 了THUAI4,算法,换了个激活函数(正态CDF),感觉分数变化相对更好了一些?
  89. 代码如下:
  90. ```cpp
  91. #include <iostream>
  92. #include <algorithm>
  93. #include <cmath>
  94. using namespace std;
  95. template <typename T>
  96. using mypair = pair<T, T>;
  97. double PHI(double x) // THUAI3: Sigmoid; THUAI4: Tanh; THUAI5: Normal Distribution CDF
  98. {
  99. //double a1 = 0.2548292592;
  100. //double a2 = -0.284496736;
  101. //double a3 = 1.421413741;
  102. //double a4 = -1.453152027;
  103. //double a5 = 1.061405429;
  104. //double p = 0.3275911;
  105. //int sign = 1;
  106. //if (x < 0)
  107. // sign = -1;
  108. //x = fabs(x) / sqrt(2.0);
  109. //double t = 1.0 / (1.0 + p * x);
  110. //double y = 1.0 - ((((((a5 * t + a4) * t + a3) * t) + a2) * t) + a1) * t * exp(-x * x);
  111. //double cdf = 0.5 * (1.0 + sign * y);
  112. //return (cdf - 0.5) * 2.0; // 化到[-1,1]之间
  113. return erf(x / sqrt(2));
  114. }
  115. // orgScore 是天梯中两队的分数;competitionScore 是这次游戏两队的得分
  116. mypair<int> cal(mypair<int> orgScore, mypair<int> competitionScore)
  117. {
  118. // 调整顺序,让第一个元素成为获胜者,便于计算
  119. bool reverse = false; // 记录是否需要调整
  120. if (competitionScore.first < competitionScore.second)
  121. {
  122. reverse = true;
  123. }
  124. else if (competitionScore.first == competitionScore.second)
  125. {
  126. if (orgScore.first == orgScore.second) // 完全平局,不改变天梯分数
  127. {
  128. return orgScore;
  129. }
  130. if (orgScore.first > orgScore.second) // 本次游戏平局,但一方天梯分数高,另一方天梯分数低,需要将两者向中间略微靠拢,因此天梯分数低的定为获胜者
  131. {
  132. reverse = true;
  133. }
  134. else
  135. {
  136. reverse = false;
  137. }
  138. }
  139. if (reverse) // 如果需要换,换两者的顺序
  140. {
  141. swap(competitionScore.first, competitionScore.second);
  142. swap(orgScore.first, orgScore.second);
  143. }
  144. // 转成浮点数
  145. mypair<double> orgScoreLf;
  146. mypair<double> competitionScoreLf;
  147. orgScoreLf.first = orgScore.first;
  148. orgScoreLf.second = orgScore.second;
  149. competitionScoreLf.first = competitionScore.first;
  150. competitionScoreLf.second = competitionScore.second;
  151. mypair<int> resScore;
  152. const double deltaWeight = 90.0; // 差距悬殊判断参数,比赛分差超过此值就可以认定为非常悬殊了,天梯分数增量很小,防止大佬虐菜鸡的现象造成两极分化
  153. double delta = (orgScoreLf.first - orgScoreLf.second) / deltaWeight;
  154. cout << "Normal CDF delta: " << PHI(delta) << endl;
  155. {
  156. const double firstnerGet = 3e-4; // 胜利者天梯得分权值
  157. const double secondrGet = 1e-4; // 失败者天梯得分权值
  158. double deltaScore = 100.0; // 两队竞争分差超过多少时就认为非常大
  159. double correctRate = (orgScoreLf.first - orgScoreLf.second) / 100.0; // 订正的幅度,该值越小,则在势均力敌时天梯分数改变越大
  160. double correct = 0.5 * (PHI((competitionScoreLf.first - competitionScoreLf.second - deltaScore) / deltaScore - correctRate) + 1.0); // 一场比赛中,在双方势均力敌时,减小天梯分数的改变量
  161. resScore.first = orgScore.first + round(competitionScoreLf.first * competitionScoreLf.first * firstnerGet * (1 - PHI(delta)) * correct); // 胜者所加天梯分
  162. if (competitionScoreLf.second < 1000)
  163. resScore.second = orgScore.second - round((1000.0 - competitionScoreLf.second) * (1000.0 - competitionScoreLf.second) * secondrGet * (1 - PHI(delta)) * correct); // 败者所扣天梯分
  164. else
  165. resScore.second = orgScore.second; // 败者拿1000分,已经很强了,不扣分
  166. }
  167. // 如果换过,再换回来
  168. if (reverse)
  169. {
  170. swap(resScore.first, resScore.second);
  171. }
  172. return resScore;
  173. }
  174. void Print(mypair<int> score)
  175. {
  176. std::cout << " team1: " << score.first << std::endl
  177. << "team2: " << score.second << std::endl;
  178. }
  179. int main()
  180. {
  181. int x, y;
  182. std::cout << "origin score of team 1 and 2: " << std::endl;
  183. std::cin >> x >> y;
  184. auto ori = mypair<int>(x, y);
  185. std::cout << "game score of team 1 and 2: " << std::endl;
  186. std::cin >> x >> y;
  187. auto sco = mypair<int>(x, y);
  188. Print(cal(ori, sco));
  189. }
  190. ```
  191. `1000 - score`(x
  192. `ReLU(1000 - score)`(√
  193. 防止真的超过了 1000)
  194. ## THUAI6
  195. ### high-ladder
  196. 因为今年的对局得分是两局得分之和,所以会出现一定程度的“数值膨胀”,在这里调低了胜者得分权值,同时提高了比赛分差距悬殊阈值和天梯分差距悬殊阈值。同时由于今年得分的上限不好确定,所以负者失分的基础值变为与胜者的得分之差。
  197. ```c++
  198. #include <iostream>
  199. #include <algorithm>
  200. #include <cmath>
  201. using namespace std;
  202. template <typename T>
  203. using mypair = pair<T, T>;
  204. // orgScore 是天梯中两队的分数;competitionScore 是这次游戏两队的得分
  205. mypair<int> cal(mypair<int> orgScore, mypair<int> competitionScore)
  206. {
  207. // 调整顺序,让第一个元素成为获胜者,便于计算
  208. bool reverse = false; // 记录是否需要调整
  209. if (competitionScore.first < competitionScore.second)
  210. {
  211. reverse = true;
  212. }
  213. else if (competitionScore.first == competitionScore.second)
  214. {
  215. if (orgScore.first == orgScore.second) // 完全平局,不改变天梯分数
  216. {
  217. return orgScore;
  218. }
  219. if (orgScore.first > orgScore.second) // 本次游戏平局,但一方天梯分数高,另一方天梯分数低,需要将两者向中间略微靠拢,因此天梯分数低的定为获胜者
  220. {
  221. reverse = true;
  222. }
  223. else
  224. {
  225. reverse = false;
  226. }
  227. }
  228. if (reverse) // 如果需要换,换两者的顺序
  229. {
  230. swap(competitionScore.first, competitionScore.second);
  231. swap(orgScore.first, orgScore.second);
  232. }
  233. // 转成浮点数
  234. mypair<double> orgScoreLf;
  235. mypair<double> competitionScoreLf;
  236. orgScoreLf.first = orgScore.first;
  237. orgScoreLf.second = orgScore.second;
  238. competitionScoreLf.first = competitionScore.first;
  239. competitionScoreLf.second = competitionScore.second;
  240. mypair<int> resScore;
  241. const double deltaWeight = 1000.0; // 差距悬殊判断参数,比赛分差超过此值就可以认定为非常悬殊了,天梯分数增量很小,防止大佬虐菜鸡的现象造成两极分化
  242. double delta = (orgScoreLf.first - orgScoreLf.second) / deltaWeight;
  243. cout << "Tanh delta: " << tanh(delta) << endl;
  244. {
  245. const double firstnerGet = 9e-6; // 胜利者天梯得分权值
  246. const double secondrGet = 5e-6; // 失败者天梯得分权值
  247. double deltaScore = 2100.0; // 两队竞争分差超过多少时就认为非常大
  248. double correctRate = (orgScoreLf.first - orgScoreLf.second) / (deltaWeight * 1.2); // 订正的幅度,该值越小,则在势均力敌时天梯分数改变越大
  249. double correct = 0.5 * (tanh((competitionScoreLf.first - competitionScoreLf.second - deltaScore) / deltaScore - correctRate) + 1.0); // 一场比赛中,在双方势均力敌时,减小天梯分数的改变量
  250. cout << "correct: " << correct << endl;
  251. resScore.first = orgScore.first + round(competitionScoreLf.first * competitionScoreLf.first * firstnerGet * (1 - tanh(delta)) * correct); // 胜者所加天梯分
  252. resScore.second = orgScore.second - round((competitionScoreLf.first - competitionScoreLf.second) * (competitionScoreLf.first - competitionScoreLf.second) * secondrGet * (1 - tanh(delta)) * correct); // 败者所扣天梯分
  253. }
  254. // 如果换过,再换回来
  255. if (reverse)
  256. {
  257. swap(resScore.first, resScore.second);
  258. }
  259. return resScore;
  260. }
  261. ```
  262. ### competition
  263. 与天梯得分算法要满足的“枫氏七条”类似,比赛得分算法也要满足“唐氏四律”,分别如下:
  264. 1. 两队经过某场比赛的得分变化,应只与该场比赛有关,而与历史积分无关。
  265. 2. 须赋予比赛获胜一方基础得分,哪怕获胜一方的优势非常小。也就是说,哪怕胜利一方仅以微弱优势获胜,也需要拉开胜者与败者的分差。
  266. 3. 胜利一方优势越大,得分理应越高。
  267. 4. 对于一场比赛,胜利一方的得分不能无限大,须控制在一个合理的数值以下。
  268. - 在非平局的情况下,(胜者)天梯得分与双方比赛分差值成正相关,得分函数如下(以x表示得分差值,y表示(胜者)天梯得分,a、b为固定参数)
  269. ​ $$y=ax^2(1-0.375\cdot(\tanh(\frac{x}{b}-1)+1))$$
  270. - 在平局情况下,(双方)天梯得分与比赛分成正相关,得分函数如下(以x表示比赛分,y表示(双方)天梯得分,c为固定参数)
  271. ​ $$y=cx^2$$
  272. - 不管是哪种情况,都有得分下界,非平局为100,平局为25
  273. ```c++
  274. #include <iostream>
  275. #include <algorithm>
  276. #include <cmath>
  277. #include <cassert>
  278. using namespace std;
  279. template <typename T>
  280. using mypair = pair<T, T>;
  281. double minScore = 100;
  282. double TieScore(double gameScore)
  283. {
  284. const double get = 9e-5; // 天梯得分权值
  285. double deltaScore = 2000.0; // 订正的幅度,该值越小,则在双方分差较大时,天梯分数改变量越小,整个函数的变化范围大约为0~2*deltaScore
  286. double highScore = 6000.0; // 将highScore设定为较大值,使得correct较小
  287. double correct = 1 - 0.375 * (tanh((highScore - deltaScore) / deltaScore) + 1.0); // 一场比赛中,在双方分差较大时,减小天梯分数的改变量
  288. cout << "correct: " << correct << endl;
  289. int score = round(gameScore * gameScore * get * correct / 4);
  290. return score > minScore / 4 ? score : minScore / 4;
  291. }
  292. double WinScore(double delta, double winnerGameScore) // 根据游戏得分差值,与绝对分数,决定最后的加分
  293. {
  294. assert(delta > 0);
  295. const double firstnerGet = 9e-5; // 胜利者天梯得分权值
  296. double deltaScore = 2000.0; // 订正的幅度,该值越小,则在双方分差较大时,天梯分数改变量越小,整个函数的变化范围大约为0~2*deltaScore
  297. double correct = 1 - 0.375 * (tanh((delta - deltaScore) / deltaScore) + 1.0); // 一场比赛中,在双方分差较大时,减小天梯分数的改变量
  298. cout << "correct: " << correct << endl;
  299. int score = round(delta * delta * firstnerGet * correct);
  300. return score > minScore ? score : minScore;
  301. }
  302. // orgScore 是天梯中两队的分数;competitionScore 是这次游戏两队的得分
  303. mypair<double> cal(mypair<double> orgScore, mypair<double> competitionScore)
  304. {
  305. // 调整顺序,让第一个元素成为获胜者,便于计算
  306. bool reverse = false; // 记录是否需要调整
  307. if (competitionScore.first < competitionScore.second)
  308. {
  309. reverse = true;
  310. }
  311. if (reverse) // 如果需要换,换两者的顺序
  312. {
  313. swap(competitionScore.first, competitionScore.second);
  314. swap(orgScore.first, orgScore.second);
  315. }
  316. double delta = competitionScore.first - competitionScore.second;
  317. double addScore;
  318. mypair<double> resScore;
  319. // 先处理平局的情况
  320. if (delta == 0)
  321. {
  322. addScore = TieScore(competitionScore.first);
  323. resScore = mypair<double>(orgScore.first + addScore, orgScore.second + addScore);
  324. }
  325. // 再处理有胜负的情况
  326. else
  327. {
  328. addScore = WinScore(delta, competitionScore.first);
  329. resScore = mypair<double>(orgScore.first + addScore, orgScore.second);
  330. }
  331. // 如果换过,再换回来
  332. if (reverse)
  333. {
  334. swap(resScore.first, resScore.second);
  335. }
  336. return resScore;
  337. }
  338. void Print(mypair<double> score)
  339. {
  340. std::cout << "team1: " << score.first << std::endl
  341. << "team2: " << score.second << std::endl;
  342. }
  343. int main()
  344. {
  345. double x, y, t, i = 0;
  346. cin >> t;
  347. while (i < t)
  348. {
  349. cout << "----------------------------------------\n";
  350. std::cout << "origin score of team 1 and 2: " << std::endl;
  351. std::cin >> x >> y;
  352. auto ori = mypair<double>(x, y);
  353. std::cout << "game score of team 1 and 2: " << std::endl;
  354. std::cin >> x >> y;
  355. auto sco = mypair<double>(x, y);
  356. Print(cal(ori, sco));
  357. ++i;
  358. }
  359. return 0;
  360. }
  361. ```