工作好久,回来打一场程序设计比赛真是神清气爽。
顺便配好了 sublime 的插件 FastOlympicCoding 以及 Terminus,前者可以方便地输入样例执行等,后者则是一个内置命令行,效率提升不少。
A. Reversort
按字面意思模拟即可。
#include <iostream>
#include <algorithm>
inline int read(){
int x=0,f=1;char ch=getchar();
while(ch<'0'||ch>'9') {if(ch=='-')f=-1;ch=getchar();}
while(ch>='0'&&ch<='9'){x=x*10+ch-'0';ch=getchar();}
return x*f;
}
int main() {
int T = read();
for(int kase=1; kase<=T; ++kase) {
int n = read(), L[128];
for(int i=1; i<=n; ++i) {
L[i] = read();
}
int ans = 0;
for(int i=1; i<n; ++i) {
int j = std::min_element(L+i, L+n+1)-L;
std::reverse(L+i, L+j+1);
ans += j-i+1;
}
printf("Case #%d: %d\n", kase, ans);
}
}
B Moons and Umbrellas
简单的动态规划,dp[i] 表示 i 位置选 C/J,之前全部确定的最小代价.
#include <bits/stdc++.h>
inline int read(){
int x=0,f=1;char ch=getchar();
while(ch<'0'||ch>'9') {if(ch=='-')f=-1;ch=getchar();}
while(ch>='0'&&ch<='9'){x=x*10+ch-'0';ch=getchar();}
return x*f;
}
char S[1024];
int dp[1024][2]; // dp[i] 表示 i 位置选 C/J,之前全部确定的最小代价
bool valid[1024][2]; // valied[i] 表示 i 位置选 C/J 是否合法
inline int valid_min(int a, int b, bool va, bool vb) {
if(va && vb) return std::min(a, b);
if(va) return a;
return b;
}
int main() {
int T = read();
for(int kase=1; kase<=T; ++kase) {
int x=read(), y=read();
scanf("%s", S);
for(int i=0; S[i]; ++i) {
valid[i][0] = valid[i][1] = 1;
if(S[i]=='C') valid[i][1] = 0; // can't be J
if(S[i]=='J') valid[i][0] = 0;
}
dp[0][0] = dp[0][1] = 0;
for(int i=1; S[i]; ++i) {
dp[i][0] = valid_min(dp[i-1][0], dp[i-1][1] + y, valid[i-1][0], valid[i-1][1]);
dp[i][1] = valid_min(dp[i-1][1], dp[i-1][0] + x, valid[i-1][1], valid[i-1][0]);
}
int n = strlen(S);
int ans = valid_min(dp[n-1][0], dp[n-1][1], valid[n-1][0], valid[n-1][1]);
printf("Case #%d: %d\n", kase, ans);
}
}
C. Reversort Engineering
下面的思路是做题的时候写在注释里的,直接 copy 出来 hhh:
找到一个长为 N 的排列,使得 reverseSort 所花费的代价恰好为 C
N <= 100, C <= 1000
注意到 reverseSort 过程中和数字绝对大小无关,只和相对大小有关,考虑递归缩小问题规模。
记 P(N,C) 为一个长为 N,reverseSort 代价为 C 的排列,即所求答案。
如果已经有 P(N-1, C-1), 想要得到 P(N, C),只需要在最前面添加一个更小的数字即可。
如果有 P(N-1, C-2),想要得到 P(N, C),需要把更小的数字插入到第一个元素后面。
如果有 P(N-1, C-3),想要得到 P(N, C),需要把前两个元素翻转,然后把更小的数字插入到第 2 个元素后面。
如果有 P(N-1, C-x),想要得到 P(N, C),需要把前 x-1 个元素翻转,然后把更小的数字插入到第 x-1 个元素后面,作为第 x 个元素
x 的取值范围 【1,N】
对于长为 N 的排列,最小的 C 是 N-1, 最大的 C 是 sum(2 … N),显然其中的每个值都可以取到。
换句话说,问题的本质相当于构造一个长为 N 的数组,第一个元素固定为 0,第 i 个元素最小值为 1,最大值为 i,使得它们的和等于 C
等价于,构造一个长为 N 的数组,第 i 个元素的最小值为 0, 最大值为 i-1,使它们的和等于 C-(N-1)
得到了这个差值数组之后,想要构建出 P(N,C) 怎么办呢,按上面的方法构造即可。
vector::insert(pos, t) 可以把 t 插入到 pos 迭代器指向的元素之前
#include <iostream>
#include <algorithm>
#include <vector>
inline int read(){
int x=0,f=1;char ch=getchar();
while(ch<'0'||ch>'9') {if(ch=='-')f=-1;ch=getchar();}
while(ch>='0'&&ch<='9'){x=x*10+ch-'0';ch=getchar();}
return x*f;
}
int main() {
int T = read();
for(int kase=1; kase<=T; ++kase) {
int N=read(), C=read();
if(C < N-1 || C > N*(N+1)/2-1) {
printf("Case #%d: IMPOSSIBLE\n", kase);
continue;
}
std::vector<int> resident(N+1); //差值数组
resident[1] = 0;
for (int i=2, tc=C-(N-1); i<=N; ++i) {
resident[i] = std::min(i-1, tc);
tc -= resident[i];
++resident[i];
}
std::vector<int> ans(2, N);
for (int i=2; i<=N; ++i) {
reverse(ans.begin()+1, ans.begin()+resident[i]);
ans.insert(ans.begin()+resident[i], N-i+1);
}
printf("Case #%d:", kase);
for(int i=1; i<=N; ++i) {
printf(" %d", ans[i]);
}
printf("\n");
}
}
D. Median Sort
给一个长为 N=50 的数组,可以询问 Q=170 次 midian, 然后给出顺序。 交互题。
维护当前排好序的元素,每次通过三分查找确定新元素的位置,可以证明最坏情况只需要 160 次询问。
三分的具体细节见注释
#include <iostream>
#include <algorithm>
#include <vector>
inline int read(){
int x=0,f=1;char ch=getchar();
while(ch<'0'||ch>'9') {if(ch=='-')f=-1;ch=getchar();}
while(ch>='0'&&ch<='9'){x=x*10+ch-'0';ch=getchar();}
return x*f;
}
int query(int a, int b, int c) {
printf("%d %d %d\n", a, b, c);
fflush(stdout);
int res = read();
if(res==-1) {
exit(0);
}
return res;
}
int answer(std::vector<int> &ans) {
for(auto x: ans) {
printf("%d ",x);
}
printf("\n");
fflush(stdout);
int res = read();
if(res==-1) {
exit(0);
}
return res;
}
/*
vector::insert(pos, t) 把 t 插入到 pos 位置指向的元素之前
在 [a, b) 范围内找到第一个迭代器,它是第一个大于 t 的元素,如果没有,就是 b.
一共有 b-a+1 个位置可以插入,尽可能均匀地分成三份
边界情况:
a=b, 没有元素了,一个插入位置,直接插入到 a 处
a+1=b,还剩一个元素,两个插入位置,不要处理这种情况,出现时扩张一位
a+2=b,还剩两个元素,三个插入位置,会使用 *a, *(a+1), id 进行一次比较,然后分割成 {a,a}, {a+1,a+1}, {a+2, a+2} 三种情况
a+3=b,还剩三个元素,四个插入位置,会使用 *(a+1), *(a+2), id 使用一次比较,然后分割成 {a,a+1}, {a+2, a+2}, {a+3, a+3}
通用分割方式:m1 = a+(b-a)/3, m2 = a+(b-a)*2/3,分割范围:{a, m1}, {m1+1, m2}, {m2+1, b}
*/
std::vector<int> ans;
using IT = std::vector<int>::iterator;
IT ternary_search(IT a, IT b, int id) {
if(a>=b) return a;
if(a+1==b) {
if(a==ans.begin()) {
return ternary_search(a, b+1, id);
} else {
return ternary_search(a-1, b, id);
}
}
IT m1 = a+(b-a)/3, m2 = a+(b-a)*2/3;
int res = query(*m1, *m2, id);
if(res == *m1) return ternary_search(a, m1, id);
if(res == id) return ternary_search(m1+1, m2, id);
/*if(res == *m2)*/ return ternary_search(m2+1, b, id);
}
int main() {
int T=read(), N=read(); read();
while(T--){
int res = query(1, 2, 3);
if(res==1) ans = std::vector<int>{2,1,3};
if(res==2) ans = std::vector<int>{1,2,3};
if(res==3) ans = std::vector<int>{1,3,2};
for(int i=4; i<=N; ++i) {
auto it = ternary_search(ans.begin(), ans.end(), i);
ans.insert(it, i);
}
answer(ans);
}
}
E. Cheating Detection
100个玩家,10000道题,各有一个 level。正常玩家做题正确的概率是 sigmoid(level_player - level_question),有一个作弊者,它有二分之一的概率一定做对,剩下的情况走正常概率。
所有 level 和结果随机生成,给定做题结果,找出作弊者,要求成功率达到 86%.
- 首先把题目按做出人数排序,把玩家按做出题目数排序,然后随机出若干个 level 按顺序分配给他们。
- 然后将题目按难度分成100块,每块100道题,计算出每个玩家做每块题目的期望正确数与实际正确数,放到一个怀疑函数里面计算怀疑值。
- 输出怀疑值最高的玩家。
提高成功率的方法:只看最难的那些题,或者给怀疑函数加上一个权重系数,越难的题权重越大,因为正常人做最难的题的成功概率不会超过 50%,而作弊者的频率一定会超过50%.
本地测试,我的成功率是 89%,提交也 AC 了。感觉还有很多可以改进的点,比如按实际做出来的题目数拟合玩家的 level,而不是按随机值分配。
推荐本地生成一组数据用于测试。
#include <iostream>
#include <algorithm>
#include <vector>
#include <cstdio>
#include <cstring>
#include <cmath>
inline int read(){
int x=0,f=1;char ch=getchar();
while(ch<'0'||ch>'9') {if(ch=='-')f=-1;ch=getchar();}
while(ch>='0'&&ch<='9'){x=x*10+ch-'0';ch=getchar();}
return x*f;
}
constexpr int P = 100, Q = 10000;
char save[128][10016];
struct Item {
int id; // id
int cnt; // 计数
double level; // 等级
double suspicion; //怀疑度
bool operator<(const Item &b) const {
return cnt<b.cnt;
}
};
Item players[128], questions[10016];
double rnd1[128], rnd2[10016];
inline double randD() {
return (double)rand()/RAND_MAX*6-3;
}
double sigmoid(double x) {
return 1.0/(1.0+exp(-x));
}
void rand_init() {
srand(114514);
for(int i=1; i<=P; ++i) rnd1[i] = randD();
for(int i=1; i<=Q; ++i) rnd2[i] = randD();
std::sort(rnd1+1, rnd1+P+1);
std::sort(rnd2+1, rnd2+Q+1);
}
void assignLevel(Item arr[], int n, double rnd[]) {
std::sort(arr+1, arr+n+1);
for(int i=1; i<=n; ++i) {
arr[i].level = rnd[i];
}
}
// 只看最后500道题,每组的怀疑值:(每组频率-每组概率)*组修正系数^2
// 可改进点:对实际 AC 的题目拟合计算玩家水平,而不是随机玩家水平
int main() {
//freopen("in2.txt", "r", stdin);
rand_init();
int T = read(); read();
int tot = 0;
for(int kase=1; kase<=T; ++kase) {
for(int p=1; p<=P; ++p) {
players[p].id = p;
players[p].cnt = 0;
}
for(int q=1; q<=Q; ++q) {
questions[q].id = q;
questions[q].cnt = 0;
}
for(int p=1; p<=P; ++p) {
scanf("%s", save[p]+1);
for(int q=1; q<=Q; ++q) {
players[p].cnt += (save[p][q]=='1');
questions[q].cnt += (save[p][q]=='0');
}
}
// 分配等级
assignLevel(players, P, rnd1);
assignLevel(questions, Q, rnd2);
int ans = 0;
for(int p=1; p<=P; ++p) {
players[p].suspicion = 0;
constexpr int groups = 100, q_in_group = Q/groups;
for(int g=groups-5; g<groups; ++g) { // 只看难题
double except = 0, occur = 0; // except 表示 q_in_group 道题中期望做对多少题,occur 表示实际做对了多少题
for(int q=g*q_in_group+1; q<=(g+1)*q_in_group; ++q) {
except += sigmoid(players[p].level - questions[q].level);
occur += (save[players[p].id][questions[q].id] == '1');
}
players[p].suspicion += (occur-except)*g*g; //只怀疑频率比概率高的人,越难的题目权重越高
}
if(ans == 0 || players[p].suspicion > players[ans].suspicion) {
ans = p;
}
}
printf("Case #%d: %d\n", kase, players[ans].id);
if(players[ans].id==59) ++tot;
}
//printf("tot = %d%%\n",tot );
}