S2考前综合刷题营Day1
打扑克
题目描述
皮蛋为了讨好黑妞,想要跟她打扑克。
他们打的扑克是这样一种规则:有面值大小从 \(1\) 到 \(n\) 的扑克各一张。其中奇数牌在皮蛋手中,偶数牌在黑妞手中。每人每次只能出一张牌,先出完者获胜(遵循最基本的扑克规则:当对手出牌后,可以选择出一张比他大的牌,或者不管,让他再任意出一张牌)。假设皮蛋和黑妞都是足够聪明的人,都想让自己获胜。现在给定 \(n\) 和谁先出牌,那么谁会获胜呢?
输入格式
第 \(1\) 行 \(1\) 个正整数 \(T\),表示数据组数。
接下来 \(T\) 行,每行 \(2\) 个正整数 \(n,op\),表示打一局牌。其中 \(n\) 如题所示,保证 \(op∈{0,1}\),\(op=0\) 表示皮蛋先出牌,\(op=1\) 表示黑妞先出牌。
输出格式
\(T\) 行每行 \(1\) 个数表示打一局牌的答案。\(0\) 表示皮蛋获胜,\(1\) 表示黑妞获胜。
样例数据
input
2
5 0
10 1
output
0
1
数据规模与约定
对于 \(40\%\) 的数据,\(n≤10\)。
对于 \(70\%\) 的数据,\(n≤10000\)。
对于 \(100\%\) 的数据,\(2≤n≤101000,T≤100\)。
时间限制:\(1s\)
空间限制:\(512MB\)
Solution
分奇偶,分先后,简单模拟找规律即可。
发现只有当牌数为奇数且皮蛋先手时,皮蛋才能赢,否则都是黑妞赢。
要注意特判 \(n=2\) 的情况。
粉刷匠
题目描述
皮蛋喜欢黑妞,想为她粉刷一面网格墙。
墙上有 \(n\) 行 \(m\) 列共 \(n∗m\) 个网格。初始时,整面墙都是红色的。皮蛋只有红、蓝两种颜色的油漆,而且皮蛋很懒,他每次刷墙只会把某一整行或某一整列刷成红色或蓝色。皮蛋一共粉刷了 \(k\) 次墙。现在给出皮蛋的粉刷方案,请你求出最后这面墙有多少个格子是蓝色的。
输入格式
第 \(1\) 行 \(3\) 个正整数 \(n,m,k\)。
接下来 \(k\) 行,每行 3$ 个整数 \(x,y,z\) 表示一次刷墙。保证 \(x,z∈{0,1}\) x,\(x=0\) 表示皮蛋粉刷第 \(y\) 行,否则表示粉刷第 \(y\) 列;\(z=0\) 则表示将该行(列)的格子都粉刷成红色,否则表示都粉刷成蓝色。保证操作合法。
输出格式
\(1\) 行 \(1\) 个数表示答案。
样例数据
input
2 2 2
0 1 1
1 2 1
output
3
数据规模与约定
对于 \(30\%\) 的数据,\(1≤n,m,k≤1000\)。
对于另外 \(30\%\) 的数据,\(n≤10,1≤m,k≤100000\)。
对于 \(100\%\) 的数据,\(1≤n,m,k≤1000000\)。
时间限制:\(2s\)
空间限制:\(512MB\)
Solution
考虑到对于每一个格子,只有最后一次的行或列的修改对它有效,所以我们可以倒着考虑。
这样,每个格子就只被修改了一次,那么对于被修改的某一行或某一列,以后不会再考虑了,所以我们可以把它删掉。
对于行的修改,如果是将这一行改为蓝色,那么我们将 \(ans+=m(m\)为列数\()\),同时 \(n--(n\)为行数);修改列同理。
#include<algorithm>
#include<iostream>
#include<cstring>
#include<cstdio>
#define ll long long
using namespace std;
int read()
{
char ch=getchar();
int a=0,x=1;
while(ch<'0'||ch>'9')
{
if(ch=='-') x=-x;
ch=getchar();
}
while(ch>='0'&&ch<='9')
{
a=(a<<1)+(a<<3)+(ch^48);
ch=getchar();
}
return a*x;
}
const int N=1e6+5;
int n,m,k;
int opt[N],x[N],y[N];
bool vis[2][N];
ll ans;
int main()
{
n=read();m=read();k=read();
for(int i=1;i<=k;i++)
{
opt[i]=read();
x[i]=read();
y[i]=read();
}
for(int i=k;i>=1;i--)
{
if(vis[opt[i]][x[i]]) continue;
vis[opt[i]][x[i]]=1;
if(opt[i]==0) ans+=m*y[i],n--;
else ans+=n*y[i],m--;
}
printf("%lld\n",ans);
return 0;
}
直线竞速
题目描述
一年一度的繁荣山庄直线竞速比赛要开始了!
本次比赛共有 \(n\) 位选手,第 \(i\) 位选手的起点在 \(a_i\) 位置,速度是 \(v_i\),速度方向是正方向。
之后有 \(Q\) 个询问,每次询问经过 \(t\) 秒后,排名在第 \(k\) 位的选手的编号(在相同位置时,编号小的排名靠前)。
输入格式
第 \(1\) 行 \(1\) 个正整数 \(n\)。
接下来 \(n\) 行,每行 \(2\) 个正整数 \(v_i,a_i\)。
接下来 \(1\) 行 \(1\) 个整数 \(Q\)。
接下来 \(Q\) 行,每行 \(2\) 个正整数 \(t,k\),表示一个询问。
输出格式
\(Q\) 行,每行一个整数表示询问的答案。
样例数据
input
4
2 100
3 50
4 60
5 1
4
1 1
50 2
60 4
100 1
output
1
4
1
4
数据规模与约定
对于 \(30\%\) 的数据,\(1≤n,Q≤1000\)。
对于另外 \(40\%\) 的数据,\(k=1\)。
对于 \(100\%\) 的数据,\(1≤n,Q≤7000\),\(1≤t≤10^9\),\(1≤v_i,a\)i≤10^5$,\(1≤k≤n\)。
时间限制:\(2s\)
空间限制:\(512MB\)
Solution
40pts
对于每次询问,\(O(n)\) 求出此刻每个人的位置,然后从大到小排序,输出第 \(k\) 大的人编号。
时间复杂度 \(O(Qn\log{n})\)。
70pts
在 \(40pts\) 的基础上,对于 \(k=1\) 的部分,我们求出每个人位置的同时维护最大位置和这个人的编号,然后输出即可。
时间复杂度 \(O(Qn)\)。
100pts
我的做法:考虑到当时间 \(t\) 逐渐增大时,决定人和人之间的位置关系的主要是他们的速度,所以我们可以求一个时间 \(T\),表示在 \(T\) 时刻之后他们的相对位置不再变化,可以求得
\(T\) 的最大范围是 \(10^5\),所以如果 \(t>=T\),我们直接输出速度排名第 \(k\) 的人的编号;否则 \(O(On\log{n})\) 暴力求得第 \(k\) 小的人的编号。
时间复杂度 \(O(Qn\log{n}-\)玄学\()\)
#include<algorithm>
#include<iostream>
#include<cstring>
#include<cstdio>
#define ll long long
using namespace std;
int read()
{
char ch=getchar();
int a=0,x=1;
while(ch<'0'||ch>'9')
{
if(ch=='-') x=-x;
ch=getchar();
}
while(ch>='0'&&ch<='9')
{
a=(a<<1)+(a<<3)+(ch^48);
ch=getchar();
}
return a*x;
}
const int N=7005;
struct node
{
int x,v,Id;
ll now;
}a[N],b[N];
int n,q;
double dp[N][N],T;
bool cmp(node x,node y)
{
if(x.v==y.v) return x.x>y.x;
return x.v>y.v;
}
bool Cmp(node x,node y)
{
if(x.now==y.now) return x.Id<y.Id;
return x.now>y.now;
}
double max(double a,double b)
{
if(a>b) return a;
return b;
}
int main()
{
n=read();
for(int i=1;i<=n;i++)
{
a[i].v=read();
a[i].x=read();
a[i].Id=i;
}
sort(a+1,a+1+n,cmp);
memset(dp,0x3fff,sizeof(dp));
for(int i=1;i<n;i++)
{
for(int j=i+1;j<=n;j++)
{
if(a[i].x>=a[j].x) dp[i][j]=0;
else
{
dp[i][j]=1.0*(a[j].x-a[i].x)/(a[i].v-a[j].v);
T=max(T,dp[i][j]);
}
}
}
q=read();
for(int i=1;i<=q;i++)
{
int t=read();
int k=read();
if(1.0*t>=T) printf("%d\n",a[k].Id);
else
{
if(k==1)
{
ll ans=-1e15,ansid;
for(int i=1;i<=n;i++)
{
a[i].now=a[i].x+t*a[i].v;
if(a[i].now>ans)
{
ans=a[i].now;
ansid=a[i].Id;
}
}
printf("%d\n",ansid);
}
else
{
for(int i=1;i<=n;i++)
{
b[i].now=a[i].x+t*a[i].v;
b[i].Id=a[i].Id;
}
sort(b+1,b+1+n,Cmp);
printf("%d\n",b[k].Id);
}
}
}
return 0;
}
正解做法:
显然,任意两位选手的相对位置最多只会变化一次,所有选手在整个比赛期间的两两相对位置的交换次数最多是 \(\frac{n(n-1)}{2}\) 。
于是我们把询问按时间排序,维护比赛期间选手的排名,对于每次询问,像冒泡排序一样从前往后把每位选手向前冒泡,总的交换次数是 \(O(n^2)\) 的。
时间复杂度 \(O(n^2+nQ+Q\log{Q})\)。
#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
#include<vector>
#include<queue>
#include<cmath>
#include<set>
#include<map>
#include<cstdlib>
#include<bitset>
#include<stack>
#include<ctime>
#include<fstream>
#define dd double
#define ll long long
#define mp make_pair
#define pb push_back
#define N 7010
#define M 1010
using namespace std;
int n,Q;
struct ma
{
int v,a,num;
}w[N];
bool cmp1(ma x,ma y)
{
return x.a>y.a;
}
struct am
{
int t,k,ans,num;
}q[N];
bool cmp2(am x,am y)
{
return x.t<y.t;
}
bool cmp3(am x,am y)
{
return x.num<y.num;
}
int main()
{
cin>>n;
for(int i=1;i<=n;i++)
{
scanf("%d%d",&w[i].v,&w[i].a);
w[i].num=i;
}
cin>>Q;
for(int i=1;i<=Q;i++)
{
scanf("%d%d",&q[i].t,&q[i].k);
q[i].num=i;
}
sort(w+1,w+n+1,cmp1);
sort(q+1,q+Q+1,cmp2);
for(int i=0;i<=Q;i++)
{
for(int j=1;j<=n;j++)
{
for(int k=j;k>1;k--)
{
ll x=w[k].a+(ll)w[k].v*q[i].t;
ll y=w[k-1].a+(ll)w[k-1].v*q[i].t;
if(y<x||y==x&&w[k-1].num>w[k].num) swap(w[k-1],w[k]);
else break;
}
}
q[i].ans=w[q[i].k].num;
}
sort(q+1,q+Q+1,cmp3);
for(int i=1;i<=Q;i++) printf("%d\n",q[i].ans);
}
游戏
题目描述
有一个游戏:给定两个正整数序列 \(A,B\),长度分别为 \(n,m\)。你可以做如下操作:删掉 \(A\) 的最后 \(x(x≥1)\) 个数并得到它们的和 \(S_1\),同时删掉 \(B\) 的最后 \(y(y≥1)\) 个数并得到它们的和 \(S_2\),这次操作的花费是 \((S_1–x)(S_2–y)\)。你需要一直操作直到 \(A,B\) 都为空(不能做完一次操作后使得其中一个为空而另一个非空)。本次游戏的总花费是每次花费的和。求最小的总花费。
输入格式
第 \(1\) 行 \(2\) 个正整数 \(n,m\)。
第 \(2\) 行 \(n\) 个正整数,表示序列 \(A\)。
第 \(3\) 行 \(m\) 个正整数,表示序列 \(B\)。
输出格式
\(1\) 行 \(1\) 个数表示最小的总花费。
样例数据
input
3 2
1 2 3
1 2
output
2
数据规模与约定
数据点 \(1:n=20,m=20\)。
数据点 \(2:n=110,m=80\)。
数据点 \(3:n=200,m=130\)。
数据点 \(4:n=400,m=80\)。
数据点 \(5:n=1000,m=333\)。
数据点 \(6:n=510,m=910\)。
数据点 \(7:n=1200,m=1400\)。
数据点 \(8:n=700,m=1800\)。
数据点 \(9:n=1998,m=1370\)。
数据点 \(10:n=2000,m=1999\)。
对于 \(100\%\) 的数据,\(1≤A_i,B_i≤1000\)。
时间限制:\(2s\)
空间限制:\(512MB\)
Solution
首先化简一下题目,把每个数 \(--\),问题就变成了区间和直接乘。
\(f[i][j]\) 表示 \(A\) 的前 \(i\) 个数和 \(B\) 的前 \(j\) 个数做游戏的最小总花费。
\(F[i][j]=min(f[k][r]+(S_A[i]-S_A[k])*(S_B[j]-S_B[r])),0<=k<i,0<=r<j\) \(S_A,S_B\) 表示 \(A,B\) 的前缀和。
时间复杂度 \(O(n^2m^2)\)。
一个结论:存在一个最优解的每次删数,至少有一段长度是 \(1\)。
于是状态转移只需要枚举一维 时间复杂度 \(O(nm(n+m))\)。
有了之前的结论,可以重新思考转移的状态:
\(1.\) 若 \(A\) 和 \(B\) 截取的最后一段长度均为 \(1\),则有转移为:\(f[i][j]=\min(f[i][j],f[i-1][j-1]+a[i]*b[j])\)。
\(2.\) 若 \(A\) 的最后一段长度为 \(1\),\(B\) 的长度不为 \(1\),则有转移 \(f[i][j]=\min(f[i][j],f[i-1][k]+a[i]*(b[k+1]+b[k+2]+...+b[j]))\)。
对照上一种情况的转移方程,换种角度来思考:我们用长度为 \(1\) 的 \(A\) 序列和长度为 \(k\) 的 \(B\) 序列对应,其实就是第一种情况执行了 \(k\) 次,因为无论如何,\(a[i]*b[j]\) 的贡献都会加进去的。
则有转移方程:\(f[i][j]=\min(f[i][j],f[i][j-1]+a[i]*b[j])\)。(就是让 \(B\) 序列的后几个依次与 \(a[i]\) 对应)。
\(3.\) 若 \(B\) 的最后一段长度为 \(1\),\(A\) 的长度不为 \(1\),此情况与情况 \(2\) 同理,转移方程为:\(f[i][j]=\min(f[i][j],f[i-1][j]+a[i]*b[i])\)。
综上,给出最后的转移方程:\(f[i][j]=\min(f[i-1][j-1],\min(f[i-1][j],f[i][j-1]))+a[i]*b[j]\)
边界条件:\(f[0][0]=0\)
时间复杂度:\(O(nm)\)。
#include<algorithm>
#include<iostream>
#include<cstring>
#include<cstdio>
#define ll long long
using namespace std;
int read()
{
char ch=getchar();
int a=0,x=1;
while(ch<'0'||ch>'9')
{
if(ch=='-') x=-x;
ch=getchar();
}
while(ch>='0'&&ch<='9')
{
a=(a<<1)+(a<<3)+(ch^48);
ch=getchar();
}
return a*x;
}
const int N=2005;
int n,m;
int a[N],b[N];
ll f[N][N];
int main()
{
n=read();m=read();
for(int i=1;i<=n;i++) a[i]=read()-1;
for(int i=1;i<=m;i++) b[i]=read()-1;
memset(f,127,sizeof(f));
f[0][0]=0;
for(int i=1;i<=n;i++)
{
for(int j=1;j<=m;j++)
{
f[i][j]=min(f[i-1][j-1],min(f[i-1][j],f[i][j-1]))+a[i]*b[j];
}
}
printf("%lld\n",f[n][m]);
return 0;
}