2018.8.8模拟祭
最惨烈的一次模拟祭(不过今天之后他就要变成第二惨烈了)
三道题一道都不会做 + 模拟时心态爆炸
不模拟永远不知道自己有多菜。不管怎么说,发现问题之后解决一下吧。
T1.rate
【题目描述】
你是一个骁勇善战、日刷百题的 OIer. 今天你已经在你 OJ 上提交了 y 次,其中 x
次是正确的,这时,你的准确率是 x/y.
然而,你最喜欢一个在 [0,1] 中的有理数 p/q(是一个既约分数),所以你希望再进
行若干次提交,使你的准确率变为 p/q. 当然,由于你的实力很强,你可以随意决定进
行多少次正确的提交和多少次错误的提交. 同时,你不希望把一天的时间都用在提交上,
所以你想求出最少还要进行多少次提交(包括正确的和错误的),才能达到这一目标.
注意:本题中,0/1 和 1/1 都是既约分数.
【输入格式】
从文件 rate.in 中读入数据。
输入第一行包含一个正整数 t(t ≤ 5 × 10 5 ),表示数据组数.
接下来 t 行,每行 4 个整数 x,y, p,q(0 ≤ x ≤ y ≤ 10 9 ;0 ≤ p ≤ q ≤ 10 9 ;y > 0;q > 0),
含义如题所述.
【输出格式】
输出到文件 rate.out 中。
输出 t 行,每行一个整数,表示使正确率达到 p/q 所需最小提交次数;若无法达
到,输出-1.
【样例 1 输入】
4
3 10 1 2
7 14 3 8
20 70 2 7
5 6 1 1
【样例 1 输出】
4
10
0
-1
对于 30% 的数据,t = 1;
对于另 30% 的数据,t ≤ 1000.
以上两部分数据中,各有 50% 的数据保证 x,y, p,q ≤ 10 4 .
这题一开始以为是一道同余方程求解的题,不过后来发现……自己忘记怎么用同余方程求解了,而且方程的系数有负数,直接使用exgcd求会出错。
后来听人讲解其实这是一道可以O(1)解决的问题。我们可以先把无解或者是输出0的情况特判掉,之后你就肯定得做题了。因为做题的时候必然总提交次数会增加,正确次数有可能增加,我们要找出最少的总提交次数,我们可以将你期望的有理数p/q的分子和分母同时扩大k倍,使其满足k*p >= x && k*q >= y && k*p-x <= k*q - y(因为你总提交次数必然大于等于正确次数)。
这样直接计算出来k = max(x/p,(y-x)/(q-p)).这里要使用double计算或是直接先去1再进1.之后计算的结果即为k * q - y.
(回家做了快2h也不知道怎么用同余方程做……数论真是不考试不知道自己有多弱……)
上代码吧……
#include<cstdio> #include<cstring> #include<algorithm> #include<queue> #include<cmath> #include<set> #define rep(i,a,n) for(int i = a;i <= n;i++) #define per(i,n,a) for(int i = n;i >= a;i--) #define enter putchar('\n') using namespace std; typedef long long ll; const ll mod = 1e9 + 7; const int M = 200005; ll read() { ll ans = 0,op = 1; char ch = getchar(); while(ch < '0' || ch > '9') { if(ch == '-') op = -1; ch = getchar(); } while(ch >= '0' && ch <= '9') { ans *= 10; ans += ch - '0'; ch = getchar(); } return ans * op; } ll t,x,y,p,q,a,b,num,k; int main() { freopen("rate.in","r",stdin); freopen("rate.out","w",stdout); t = read(); while(t--) { x = read(),y = read(),p = read(),q = read(); if(p == q) { if(x == y) printf("0\n"); else printf("-1\n"); continue; } if(p == 0) { if(x == 0) printf("0\n"); else printf("-1\n"); continue; } k = max((x - 1) / p,(y - x - 1) / (q - p)) + 1; printf("%lld\n",k * q - y); } return 0; } /* 1 3 10 1 2 */
T2.exam【题目描述】你很快地完成了所有的题目,并且使准确率达到了 p/q,于是开始无所事事. 老师看见了,对你说:“你今天做的题够多的了,别再做了. 我们接下来 k 天每天都要模拟,我这里有很多题,你去选一些题目编 k 场模拟赛吧. ”老师将所有题目都编了序号,还给你了 n 种出题方案(所有方案两两不同). 每种出题方案选择所有编号属于 [l,r] 的题目,作为一天的试题. 现在,你需要选出 k 种出题方案(一个方案只能选取一次),分别对应 k 天的模拟赛. 老师非常强调复习的重要性,因此希望有一些题目每天都出现在模拟赛当中,你需要最大化每天都出现的题目的数量.
【输入格式】从文件 exam.in 中读入数据。
输入第一行包含两个正整数 n,k(1 ≤ k ≤ n ≤ 3 × 10 ^5 ),分别为出题方案数和模拟赛
天数.接下来 n 行,每行两个整数 l,r(−10 ^9 ≤ l ≤ r ≤ 10 ^9 ),表示一个出题方案(保证所有方案两两不同).
【输出格式】
输出到文件 exam.out 中。
输出一个非负整数,表示每天都出现的题目的数量的最大值.
【样例 1 输入】
4 2
1 100
40 70
120 130
125 180
【样例 1 输出】
31
【子任务】
对 10% 的数据,k = n;
对另 20% 的数据,k = 2;
对另 20% 的数据,1 ≤ k ≤ n ≤ 20;
其余 5 组数据,n = 10^ 2 ,10 ^3 ,10^ 4 ,10 ^5 ,3 × 10 ^5 .
考试的时候心态很爆炸……这道题就没想出来……只过了一个10pts的n == k的情况。
后来发现其实并没有想象的那么难。我们从n段区间之内选k段区间,使其公共区间最长,这k段区间肯定不是乱选的,肯定是他们之间靠得近,公共区间长度要更大一些。
所以我们老套路,按左端点排序,使用优先队列维护当前在堆中的k个元素的右端点最靠左的位置。这样的话枚举的时候左端点是保证单调的,而且我们每次取肯定都是取当前最靠右的一个,其他根本不用考虑,所以左端点就不用维护。每次遇到新的区间更新一下答案。然后,只有在当前区间的右端点比堆中右端点大我们才会把这个区间压进来并且将堆首区间删除。因为左端点肯定是单调不减的,如果右端点比当前还小的话取它就没有什么意义。
这样扫一波就过了,复杂度O(nlogn)上代码。
#include<cstdio> #include<cstring> #include<algorithm> #include<queue> #include<cmath> #include<set> #define rep(i,a,n) for(int i = a;i <= n;i++) #define per(i,n,a) for(int i = n;i >= a;i--) #define enter putchar('\n') using namespace std; typedef long long ll; const ll mod = 1e9 + 7; const int M = 300005; ll read() { ll ans = 0,op = 1; char ch = getchar(); while(ch < '0' || ch > '9') { if(ch == '-') op = -1; ch = getchar(); } while(ch >= '0' && ch <= '9') { ans *= 10; ans += ch - '0'; ch = getchar(); } return ans * op; } struct seg { ll l,r; bool operator < (const seg &g) const { return l < g.l; } }e[M]; ll n,k,cur,ans; priority_queue <int,vector<int>,greater<int> > q; int main() { freopen("exam.in","r",stdin); freopen("exam.out","w",stdout); n = read(),k = read(); rep(i,1,n) e[i].l = read(),e[i].r = read(); sort(e+1,e+1+n); rep(i,1,k-1) q.push(e[i].r); cur = q.top(); rep(i,k,n) { ans = max(ans,min(cur,e[i].r) - e[i].l + 1); if(e[i].r > cur) q.pop(),q.push(e[i].r),cur = q.top(); } printf("%lld\n",ans); return 0; }
T3.talk
【题目描述】
k 天的模拟已经结束,老师根据大家在模拟赛中的表现,给每人了一个正整数评分(介于 [1,8] 之间). 而同学们在机房中坐成了一排,共 n 个同学,从左到右同学的得分依次为 a 1 ,a 2 ,···a n . 为了更好地了解同学们的学习状况,以制定下一步的学习计划,现在要选出一些人开总结会. 要求如下:
• 未被选中的同学离开,选中的同学在原位不动(也就是说被选中的同学的相对位置不变,亦即选择原序列的一个子序列)(免去了换座位的麻烦);
• 任意两种评分被选中的的人数相差不超过 1(体现平等原则);
• 选出的子序列中具有相同得分的同学相邻(不是指在原座次中相邻,是在子序列中相邻)(方便同学们更好地讨论);
同时老师希望在满足以上三点的前提下选出的总人数越多越好.
举例来说,若选出的序列为 [1,1,2,2],则违反了第二条规定(得分 3 到 8 均选择了 0 人);选出的序列为 [1,2,2,1],则既违反了第二条规定,也违反了第三条规定.
【输入格式】
从文件 talk.in 中读入数据。
输入第一行包含一个正整数 n(n ≤ 1000),学生总数.
第二行包含 n 个整数,为从左到右学生的得分.
【输出格式】
输出到文件 talk.out 中。
输出一个整数,表示最多选取的人数.
【样例 1 输入】
3
1 1 1
【样例 1 输出】
1
对于 20% 的数据,没有人评分为 8 分;
对于另 20% 的数据,n ≤ 20;
对于另 20% 的数据,n ≤ 100.
这题看数据范围和题目描述应该是DP,不过以我的智商难以推出DP方程(以你的智商甚至都没意识到没有8有什么含义,而且爆搜也写不对)
于是瞎写一波4pts滚粗
正解是状压DP+二分答案。这里我用的是std的DP方法,用dp[i][j]表示枚举到第i位,状态为j时,长度为len(就是你当前二分的值)+1的连续数字序列有多少个。其中状态j是一个8位二进制数的十进制表示。
其中,从右向左数第k位为1表示已经被选取,为0表示未被选取。因为要求每种数字都必须是连续的,所以我们可以在DP的时候直接转移到从当前位置获取长度为len的某个数的序列要转移到的下一个位置是哪里,这样进行DP。
需要事先用vector存一下每种数字出现的所有位置,在DP过程中需要开数组记录一下这一位数字在之前出现过了多少次。(就是出现过了但是你由于种种原因并没有选择)
对于每一次二分的答案,返回的是在任意一位,符合八种状态全部选满的情况(即所有的数字都被选取了长度为len/len+1的序列)。然后如果不符合的话就往小二分,否则往大二分。
然后这样二分有一个弊端就是你不能最后精确到一个数,因为有可能并没有所有数字都出现全,那样无论你二分什么答案返回值都是-1.所以我们留一点余地,保留两个值,如果这两个值返回的都是-1,那么直接找一下,有几种数字出现过,那么就能选择几个人。
在转移的时候要使用&和|来判断当前是否可行和你的转移位置。如果当前转移的数字不够用则直接返回。
上代码看一下(你的状压DP太弱了该好好练了)
#include<cstdio> #include<cstring> #include<algorithm> #include<queue> #include<cmath> #include<set> #include<vector> #define rep(i,a,n) for(int i = a;i <= n;i++) #define per(i,n,a) for(int i = n;i >= a;i--) #define enter putchar('\n') using namespace std; typedef long long ll; const ll mod = 1e9 + 7; const int M = 1005; int read() { int ans = 0,op = 1; char ch = getchar(); while(ch < '0' || ch > '9') { if(ch == '-') op = -1; ch = getchar(); } while(ch >= '0' && ch <= '9') { ans *= 10; ans += ch - '0'; ch = getchar(); } return ans * op; } vector <int> v[10]; int n,a[M],dp[M][1<<8],cur[10],l,r,mid,ans; int check(int len) { memset(cur,0,sizeof(cur)); memset(dp,-1,sizeof(dp)); dp[1][0] = 0; rep(i,1,n) { rep(j,0,(1<<8)-1) { if(dp[i][j] == -1) continue;//如果无法转移直接返回 rep(k,1,8) { if(j & (1 << (k-1))) continue;//如果这位数字已经选过就返回 int x = cur[k] + len - 1; if(x >= v[k].size()) continue;//如果超出就返回 dp[v[k][x]][j|(1<<(k-1))] = max(dp[i][j],dp[v[k][x]][j|(1<<(k-1))]);//进行转移 x++; if(x >= v[k].size()) continue; dp[v[k][x]][j|(1<<(k-1))] = max(dp[i][j]+1,dp[v[k][x]][j|(1<<(k-1))]); //转移长度为len+1的情况 } } cur[a[i]]++; } int now = -1; rep(i,1,n) now = max(now,dp[i][(1<<8)-1]);//能选取len-1的数字个数的最大值 if(now == -1) return -1; return now * (len + 1) + (8 - now) * len;//计算总长度 } int main() { freopen("talk.in","r",stdin); freopen("talk.out","w",stdout); n = read(); rep(i,1,n) a[i] = read(),v[a[i]].push_back(i); l = 1,r = n / 8; while(l + 1 < r) { mid = l + r >> 1; if(check(mid) != -1) l = mid; else r = mid - 1; } ans = max(check(l),check(r)); if(ans == -1) { ans = 0; rep(i,1,8) if(v[i].size()) ans++;//有数字未出现过的情况 } printf("%d\n",ans); return 0; }
唉……还有7h他就要变成第二惨模拟了……
今日爆零滚粗预定。