qbxt Day 1
Day 1
考试题解
T1 打扑克
唯一切掉的题,终于签到成功了。本题其实非常简单,打斗地主打多了的人真的是一眼秒。一看数据1000位,看都不用看就是找规律。再看分成奇数偶数两组,就想到通过奇偶性来分类讨论一波。手玩几组小数据,很容易发现规律:一共就四种情况:1.共奇数张牌,奇数先出。2.共奇数张牌,偶数先出。3.共偶数张牌,偶数先出。4.共偶数张牌,奇数先出。自己推导一下就能发现,只有总数是奇数并且奇数先出奇数才能赢,其他都会输,所以直接判字符串末位的奇偶性即可。但是有一种特殊情况,就是\(n\)是\(2\)的时候是谁先出谁赢,本题共有80分有这个点。。。。。所以说很多人没有考虑到就挂了\(80\),还是要注意细节。
#include<iostream>
#include<algorithm>
#include<cstdio>
#include<cmath>
#include<cstring>
using namespace std;
inline int read(){
int x=0,f=1;char ch=getchar();
while(!isdigit(ch)){if(ch=='-') f=-1;ch=getchar();}
while(isdigit(ch)){x=x*10+ch-48;ch=getchar();}
return x*f;
}
int T,op;
char n[1010];
int main()
{
T=read();
while(T--){
cin>>n;
op=read();
if(strlen(n)==1&&n[0]=='2'){
if(op==0){
printf("0\n");
continue;
}
else{
printf("1\n");
continue;
}
}
if((n[strlen(n)-1]-'0')%2==1){//奇数张牌
if(op==0){//
printf("0\n");
}
else{
printf("1\n");
}
}
else{
if(op==0){
printf("1\n");
}
else{
printf("1\n");
}
}
}
return 0;
}
T2 粉刷匠
考试时觉得是道可做题,推容斥能够推出来。先写了个暴力\(O(k(m+n))\),拿到\(30\)。但结果就是我越推越乱越推越乱,从没跟暴力拍上过,做了大约一个小时有点自闭,然后去做\(T3\)了。打完\(T3\)暴力,看了眼\(T4\),觉得不可做就又回来做\(T2\)。然后一直推到最后遗憾离场,\(30pts\)滚粗。中午回去巨神\(CYC\)告诉可以倒着做,因为倒着做就不用再考虑会不会被覆盖了。我恍然大悟,然后回来切掉了这道题。
其实这道题在某些方面跟去年江西\(CSP-S\)的\(T3\)网格图中间求和的那部分非常非常相似,如果一行被染成了蓝色,那么就看看有几列已经被确定了,减去这几列即可。染成红色的话只需要去掉对答案的贡献这一行代码即可。
#include<iostream>
#include<algorithm>
#include<cstdio>
#include<cmath>
#include<cstring>
using namespace std;
typedef long long ll;
inline ll read(){
ll x=0,f=1;char ch=getchar();
while(!isdigit(ch)){if(ch=='-') f=-1;ch=getchar();}
while(isdigit(ch)){x=x*10+ch-48;ch=getchar();}
return x*f;
}
ll n,m,k,ans,lsum,hsum;
ll x[1000005],y[1000005],z[1000005];
bool hang[1000005],lie[1000005];
int main()
{
scanf("%lld%lld%lld",&n,&m,&k);
for(ll i=1;i<=k;i++){
scanf("%lld%lld%lld",&x[i],&y[i],&z[i]);
}
for(ll i=k;i>=1;i--){
if(z[i]==1){
if(x[i]==0){
if(!hang[y[i]]){
hang[y[i]]=1;
ans+=(m-lsum);
hsum++;
}
}
else{
if(!lie[y[i]]){
lie[y[i]]=1;
ans+=(n-hsum);
lsum++;
}
}
}
else{
if(x[i]==0){
if(!hang[y[i]]){
hang[y[i]]=1;
hsum++;
}
}
else{
if(!lie[y[i]]){
lie[y[i]]=1;
lsum++;
}
}
}
}
printf("%lld\n",ans);
return 0;
}
// 不开longlong见祖宗
拓展:疯狂的馒头
T3 直线竞速
首先一眼看出了\(70pts\)的做法,只需要暴力算出查询的每一秒的时候每一个人的位置然后排序即可,时间复杂度是\(O(Qnlogn)\),这样能得到\(30pts\)。但发现还有\(40pts\)是只求第一名,那么在算每一个人的位置时直接在循环中更新最大值即可,时间复杂度是\(O(Qn)\)。就得到了\(70pts\)。
但是令我万万没想到的是,我离正解只差了一个\(STL\)函数!!!在神奇的\(STL\)库里,有一个神奇的函数叫做\(nth_element()\)。那么这个函数的意思是什么呢:在O(n)的时间复杂度内,在无序序列中,找到第\(k\)小的数。
我:???????
然后用了函数之后总时间复杂度就到了\(O(Qn)\),可以切掉这道题。
#include<iostream>
#include<algorithm>
#include<cstdio>
#include<cmath>
#include<cstring>
using namespace std;
inline int read(){
int x=0,f=1;char ch=getchar();
while(!isdigit(ch)){if(ch=='-') f=-1;ch=getchar();}
while(isdigit(ch)){x=x*10+ch-48;ch=getchar();}
return x*f;
}
int n,Q,t,k,num1;
int v[7010],a[7010];
long long maxx;
struct F{
int num;
long long s;
}f[7010];
long long cmp(F A,F B){
return A.s>B.s;
}
int main()
{
n=read();
for(int i=1;i<=n;i++){
v[i]=read();
a[i]=read();
}
Q=read();
for(int i=1;i<=Q;i++){
t=read();
k=read();
maxx=0;
for(int j=1;j<=n;j++){
f[j].s=a[j]+1ll*v[j]*t;
f[j].num=j;
if(f[j].s>maxx){
maxx=f[j].s;
num1=f[j].num;
}
}
if(k==1){
printf("%d\n",num1);
continue;
}
nth_element(f+1,f+k,f+1+n,cmp);//并不知道咋用,瞎搞一波然后对了
printf("%d\n",f[k].num);
}
return 0;
}
但是老师的正解似乎并不是这样。。。(这做法确实nc而且low)
老师承认这题忘记了有这个函数,出水了,如果手写分治拿到满分也可以接受,但是\(STL\)就过分简单了。
下面说一下分治求第\(k\)小元素的原理:随便找一个元素放在中间当作挡板,分成两半,将小于这个数的元素放在左边,反之放在右边。然后看看左边数的个数和右边数的个数。如果左边的数的个数不到\(k\),那么数肯定在右边,就相当于求右边序列中的第\(k-x\)(左边有\(x\)个数)小的数,递归求解即可,复杂度是\(O(n)\)。
正解做法2:
在起点处排序,每个选手之间的相对位置不可能跨越交换,只能相邻两人直接交换。这与冒泡排序的思想类似,因为速度快的超过速度慢的之后就不会再被慢的人超过,所以最多交换\(n^2\)次。就相当于每次交换减少一对逆序对。这不就是冒泡排序?!所以做法就是先提前将询问的时间排序,求出每个人那一秒在多远的位置,然后将所有的逆序对交换,然后输出排名。由于交换后不会再换回来,所以下一次询问在当前序列基础上继续操作即可。由于最多产生\(n^2\)个逆序对,所以最多交换\(n^2\)次,总时间复杂度是\(O(n^2+QlogQ+nQ)\)。
T4 游戏
?分做法:
\(f_{i,j}\)表示\(A\)的前\(i\)个数和\(B\)的前\(j\)个数做游戏的最小总花费。\(f_{i,j}=min(f_{k,r}+(SA_i-SB_k)*(SA_j-SA_r),0<=k<i,0<=r<j\)(这式子没记全QWQ)。\(SA\)和\(SB\)表示\(A\)和\(B\)的前缀和。总时间复杂度:\(O(n^2m^2)\)。
100分做法:
发现一结论:每次取的两端中至少有一段中只有一个数。假设\(A\)序列我分成两段取,两段的值分别是\(x\),\(y\)。\(B\)序列也如此,分为\(a\),\(b\)两段。那么合起来的贡献就是\((x+y)*(a+b)\)要比分开的贡献\((ax+by)\)大,所以显然分开更优。就这样一直分下去,发现至少有一个序列在每一次操作的时候只选一个数最优,那么枚举是哪一段最后包含那一个只有一个数的那一段即可。用\(f_{i_j}\)来记录\(A\)序列选\(i\)个数然后\(B\)序列选\(j\)个数的时候最小贡献是多少。那么考虑转移方程:当两个序列都只选一个数的时候,那么很明显,就是由各自长度-1然后加上当前两个数的乘积。但如果不是的话,则可以看做是此过程重复多次。比如说\(A\)序列取一个数,然后\(B\)序列取了任意多个数。则可以看做\(A\)序列没有删掉最后一个数,然后一直乘下去直到乘完这些数,那么转移就是\(f_{i,j}=f_{i,j-1}+a_i*b_j\)。同理,就可以得到三个不同状态,然后取最小值,即是答案。
#include<iostream>
#include<cstdio>
#include<cmath>
#include<cstring>
#include<cstdlib>
#include<algorithm>
#include<queue>
using namespace std;
typedef long long int ll;
int n,m;
int a[2010],b[2010];
ll f[2010][2010];
int main()
{
scanf("%d%d",&n,&m);
for(int i=1;i<=n;i++){
scanf("%d",a+i);
a[i]--;
}
for(int i=1;i<=m;i++){
scanf("%d",b+i);
b[i]--;
}
memset(f,0x3f,sizeof(f));//初始化为最大值
f[0][0]=0;//什么都没选贡献当然是0
for(int i=1;i<=n;i++){
for(int j=1;j<=m;j++){
ll x=a[i]*b[j];
f[i][j]=min(f[i-1][j-1],min(f[i-1][j],f[i][j-1]))+x;//三个不同的状态
}
}
printf("%lld\n",f[n][m]);
return 0;
}
DFS
NOI 1999 生日蛋糕
常规操作:如果已经大于\(ans\)直接\(return\)。
最优性剪枝:\(v=pi*r^2*h,s=2*pi*r*h,s=2*v/r\),所以当体积一定时,r最大的时候最优。然后将半径带入体积求出高度继续搜索。
可行性剪枝:如果余下的蛋糕圆柱体不能满足半径和高都小于当前蛋糕圆柱体(当前圆柱体过小)。如果叠最小的圆柱体都会超出体积,那么也剪掉。
BFS
HAOI 2008 移动玩具
把所有边建出来然后\(BFS\)。
拓扑序明显的时候用\(dp\),常数小。