信息学联赛模拟题2+解析+代码
一. 题目描述:
题目中文名 | 怪兽 | 白板 | 序列 | 游戏 |
---|---|---|---|---|
题目英文名 | monster | sheet | sequence | game |
提交源程序文件名 | monster.cpp | sheet.cpp | sequence.cpp | game.cpp |
输入文件名 | monster.in | sheet.in | sequence.in | game.in |
输出文件名 | monster.out | sheet.out | sequence.out | game.out |
每个测试点时限 | 1s | 1s | 1s | 1s |
测试点数量 | 20 | 20 | 20 | 20 |
每个测试点分值 | 5 | 5 | 5 | 5 |
内存限制 | 512MB | 512MB | 512MB | 512MB |
二. 注意事项:
1、文件名(程序名和输入输出文件名)必须使用英文小写。
2、 C/C++ 中函数 main() 的返回值类型必须是 int, 程序正常结束时的返回值必须是 0。
3、全国统一评测时采用的机器配置为: Intel® Core™ i7-7600U CPU @ 2.60GHz, 内存
16GB,编译版本为 4.8.4,在 Linux 下评测。上述时限以此配置为准。
4、只提供 Linux 格式附加样例文件。
5、特别提醒:评测在当前最新公布的 NOI Linux 下进行, 各语言的编译器版本以其为准。
1 怪兽
(monster.cpp/c/pas)
【问题描述】
PYWBKTDA 最近正在打怪兽, 一个斯拉夫神话中的凶猛怪兽, 一个有着多个头的巨大龙状爬
行动物。
开始的时候, 怪兽有 X 个头, 你有 n 种打击方式。如果你选择第 i 种打击方式, 这个神奇的怪兽会减少 min(di, cur) 个头。这里 cur 表示当前怪兽拥有的头的数量。但是如果怪兽被打击以后还至少留下了一个头, 那么它就会再长出 hi 个头来。当 cur = 0 或者小于 0 的时候, 怪兽被打败了。
注意, 你可以使用任何一种打击方式任何次数, 以任何的顺序。
例如, 如果当前 cur = 10, d = 7, h = 10, 那么一次打击以后怪兽就会有 13 个头了(因为减少了7 个头以后, 怪兽还剩下 3 个头, 再加上 10 个头)。但是如果当前 cur = 10, d = 11, h = 100, 那么怪兽就被打败了。
【输入格式】
第一行输入是两个整数 n 和 x, 分别表示打击的种类和开始时候怪兽的头的数量。
接下来 n 行, 每行两个整数描述了 di 和 hi, 表示第 i 种打击减少的头的数量和会长出来的头的
数量。
【输出格式】
输出只有一个整数, 表示最少需要打击的次数, 如果怪兽无法被打败, 就输出 −1。
【输入输出样例 1】
3 10
6 3
8 2
1 4
2
你可以使用第一种打击方式, 第一次打击以后剩下 (10-6+3=7) 个头, 然后再用第 2 个打击方
式。
【输入输出样例 2】
4 10
4 1
3 2
2 6
1 100
3
你可以使用第一种打击方式, 攻击 3 次。
【输入输出样例 3】
2 15
10 11
14 100
-1
这里你无法打败怪兽。
【数据范围】
对于 50% 的数据,
1
≤
n
≤
10
,
1
≤
x
≤
100
,
1
≤
d
i
≤
100
,
1
≤
h
i
≤
100
1 ≤ n ≤ 10, 1 ≤ x ≤ 100, 1 ≤ d_i ≤ 100, 1 ≤ h_i ≤ 100
1≤n≤10,1≤x≤100,1≤di≤100,1≤hi≤100。
对于 100% 的数据,
1
≤
n
≤
100
,
1
≤
x
≤
1
0
9
,
1
≤
d
i
≤
1
0
9
,
1
≤
h
i
≤
1
0
9
1 ≤ n ≤ 100, 1 ≤ x ≤ 10^9, 1 ≤ d_i ≤ 10^9, 1 ≤ h_i ≤ 10^9
1≤n≤100,1≤x≤109,1≤di≤109,1≤hi≤109。
【解析】
贪心+枚举
记录最大的
d
i
d_i
di ,和最大的
d
i
−
h
i
d_i-h_i
di−hi,最后一下用
m
a
x
(
d
i
)
max(d_i)
max(di)打,之前则一直用
m
a
x
(
d
i
−
h
i
)
max(d_i-h_i)
max(di−hi) ,进行计算即可。
【代码】
#include <bits/stdc++.h>
using namespace std;
int main() {
freopen("monster.in","r",stdin);
freopen("monster.out","w",stdout);
int t,n,x;
t=1;
while(t--) {
scanf("%d%d",&n,&x);
int a=-1e9,b=0;
for(int i=1; i<=n; i++) {
int p,q;
scanf("%d%d",&p,&q);
a=max(a,p-q);
b=max(b,p);
}
if(a<=0 && b<x) printf("-1\n");
else {
if(b>=x) printf("1\n");
else printf("%d\n",1+(x-b+a-1)/a);
}
}
return 0;
}
2 白板
(sheet.cpp/c/pas)
【问题描述】
PYWBKTDA 有一块白板, 这块白板的四条边分别平行于坐标轴。我们可以假设这块白板的左
下角在 (x1, y1) 位置, 右上角在 (x2, y2) 位置。
现在有两块黑板放到白板的上面, 这两块黑板的四条边也是平行于坐标轴的。我们可以设第 1 块黑板的左下角是 (x3, y3), 右上角在 (x4, y4) 位置, 第 2 块黑板的左下角是 (x5, y5), 右上角在 (x6, y6)位置。
现在你的任务是来判断, 我们从上往下看, 是否有白板的部分区域可以被看到。所谓的白板部分区域被看到的意思是, 白板上至少有一个点没有被黑板遮住。
【输入格式】
输入第一行有一个整数 t, 表示数据的组数。
接下来每组数据:
输入的第一行包含 4 个整数, 分别表示 x1, y1, x2, y2, 即白板的左下角和右上角。
输入的第二行包含 4 个整数, 分别表示 x3, y3, x4, y4, 即第 1 块黑板的左下角和右上角。
输入的第三行包含 4 个整数, 分别表示 x5, y5, x6, y6, 即第 2 块黑板的左下角和右上角。
【输出格式】
输出有 t 行, 对于每组数据, 如果有部分白板可以被看到, 就输出 Y ES, 否则就输出 NO。
【输入输出样例】
3
2 2 4 4
1 1 3 5
3 1 5 5
3 3 7 5
0 0 4 6
0 0 7 4
5 2 10 5
3 1 7 6
8 1 11 7
NO
YES
YES
第一个例子中, 白板被黑板完全覆盖。
第二个例子中, 部分白板可以看到。比如 (6.5,4.5) 这个点就可以被看到。
【数据范围】
对于 50% 的数据:
0
≤
x
1
<
x
2
≤
100
,
0
≤
y
1
<
y
2
≤
100
。
0
≤
x
3
<
x
4
≤
100
,
0
≤
y
3
<
y
4
≤
100
。
0
≤
x
5
<
x
6
≤
100
,
0
≤
y
5
<
y
6
≤
100
。
0 ≤ x1 < x2 ≤ 100, 0 ≤ y1 < y2 ≤ 100。\\ 0 ≤ x3 < x4 ≤ 100, 0 ≤ y3 < y4 ≤ 100。\\ 0 ≤ x5 < x6 ≤ 100, 0 ≤ y5 < y6 ≤ 100。
0≤x1<x2≤100,0≤y1<y2≤100。0≤x3<x4≤100,0≤y3<y4≤100。0≤x5<x6≤100,0≤y5<y6≤100。
对于 100% 的数据:
1
≤
t
≤
100
。
0
≤
x
1
<
x
2
≤
1
0
6
,
0
≤
y
1
<
y
2
≤
106
。
0
≤
x
3
<
x
4
≤
1
0
6
,
0
≤
y
3
<
y
4
≤
106
。
0
≤
x
5
<
x
6
≤
106
,
0
≤
y
5
<
y
6
≤
106
。
1 ≤ t ≤ 100。\\ 0 ≤ x1 < x2 ≤ 10^6, 0 ≤ y1 < y2 ≤ 106。\\ 0 ≤ x3 < x4 ≤ 10^6, 0 ≤ y3 < y4 ≤ 106。\\ 0 ≤ x5 < x6 ≤ 106, 0 ≤ y5 < y6 ≤ 106。
1≤t≤100。0≤x1<x2≤106,0≤y1<y2≤106。0≤x3<x4≤106,0≤y3<y4≤106。0≤x5<x6≤106,0≤y5<y6≤106。
【解析】
可以离散化。
分情况:
只有4种情况白板被黑板覆盖:
1.被第一块完全覆盖。
2.被第二块完全覆盖。
3.两块板上下覆盖。
4.两块板左右覆盖。
判断一下即可。
【代码】
#include <bits/stdc++.h>
using namespace std;
int x[7],y[7],tx[7],ty[7];
int inner(int x1,int y1,int x2,int y2,int x3,int y3,int x4,int y4) {
return x1<=x3 && x4<=x2 && y1<=y3 && y4<=y2;
}
int main() {
freopen("sheet.in","r",stdin);
freopen("sheet.out","w",stdout);
int t;
cin >> t;
while(t--) {
for(int i=1; i<=6; i++)
scanf("%d%d",&x[i],&y[i]);
for(int i=1; i<=6; i++)
tx[i]=x[i],ty[i]=y[i];
sort(tx+1,tx+7);
sort(ty+1,ty+7);
int m1=unique(tx+1,tx+7)-tx-1;
int m2=unique(ty+1,ty+7)-ty-1;
int s1,s2,s3,s4;
for(int i=1; i<=m1; i++) {
if(tx[i]==x[1]) s1=i;
if(tx[i]==x[2]) s2=i;
}
for(int i=1; i<=m2; i++) {
if(ty[i]==y[1]) s3=i;
if(ty[i]==y[2]) s4=i;
}
int check=0;
for(int i=s1; i<s2; i++) {
for(int j=s3; j<s4; j++) {
if(!inner(x[3],y[3],x[4],y[4],tx[i],ty[j],tx[i+1],ty[j+1]) && !inner(x[5],y[5],x[6],y[6],tx[i],ty[j],tx[i+1],ty[j+1]))
check=1;
}
}
puts(check ? "YES" : "NO");
}
return 0;
}
3 序列
(sequence.cpp/c/pas)
【问题描述】
PYWBKTDA 最近在研究一些有趣的数列。
他给你一个无穷序列”112123123412345… ”。这个序列是这么构造的, 我们先写出 1 到 1 的所有整数, 然后写出 1 到 2 的所有整数, 然后写出 1 到 3 的所有整数, 这样不断重复进行到无穷。
例如现在前 56 个数字组成的序列是:
“11212312341234512345612345671234567812345678912345678910”。
我们把序列里面的每个数字从 1 开始编号, 比如编号为 1 的数字就是 1, 编号为 2 的数字也是1, 编号为 3 的数字是 2, 编号为 20 的数字是 5, 编号为 38 的数字是 2, 编号 56 的数字是 0.
现在你的任务是对于题目给出的 q 个询问, 每个询问是
k
i
k_i
ki, 表示问你编号为
k
i
k_i
ki 的数字是多少。
【输入格式】
第一行是整数 q, 表示询问的数量。
接下来 i 行, 每行一个整数, 表示
k
i
k_i
ki。
【输出格式】
输出共计有 q 行, 第 i 行的输出表示对应的 k i k_i ki 的询问, 显然, 每个答案都在 0 到 9 之间。
【输入输出样例 1】
5 1 3
20
38
56
1
2
5
2
0
这组数据就是题目描述里面提到的。
【输入输出样例 2】
4
2132
506
999999999
1000000000
8
2
9
8
【数据范围】
对于 30% 的数据,
1
≤
k
i
≤
1
0
5
1 ≤ k_i ≤ 10^5
1≤ki≤105。
对于 70% 的数据,
1
≤
k
i
≤
1
0
9
1 ≤ k_i ≤ 10^9
1≤ki≤109。
对于 100% 的数据,
1
≤
k
i
≤
1
0
18
,
1
≤
q
≤
500
1 ≤ k_i ≤ 10^{18}, 1 ≤ q ≤ 500
1≤ki≤1018,1≤q≤500。
【解析】
30%做法 暴力
70%做法 二分
100%做法 二分+ 数位dp
先考虑一个简单点的问题,求 1234567891011… 中第 n 位数字。
这个可以每次减9×1,90×2,900×3… 直到不能减,假设还剩下 x,要减 cnt 位数,那么答案就是 9999999…(cnt-1个)+x/cnt个 的第 x%cnt 位。时间复杂度O(lgn)。
我们考虑怎么把原问题转化为这个简单问题。
设 len(n) 为1 , 1~2 ,… , 1~n 组成的序列的长度。
这个可以 O(lgn) 求出,或者 O(n) 递推,但要达到 1018, n应该在几十亿,我们不使用递推。
然后二分求出最大的 n,其 len(n)<k,那么问题就转换为求 1234567891011… 中第 k-len(n)位数。
坑点:二分范围要取适当,太大 len(n)会爆long long ,太小解不了大数据。
【代码】
#include <bits/stdc++.h>
using namespace std;
#define ull unsigned long long
int const N=20;
ull const inf=1e18;
ull dp[N][N],pw[N];
inline void up(ull &x) {
if(x>=inf) x=inf;
}
inline void init() {
pw[0]=1;
for(int i=1; i<19; i++)
pw[i]=pw[i-1]*10;
for(int i=1; i<=9; i++)
dp[1][i]=dp[1][i-1]+1;
for(int i=2; i<19; i++)
for(int j=1; j<10; j++) {
for(int k=1; k<=i; k++) {
ull st=j*pw[i-1]-(pw[k-1]-1);
ull ed=(j+1)*pw[i-1]-pw[k-1];
ull num=(ed-st+1);
dp[i][j]+=(st+ed)*num/2;
up(dp[i][j]);
}
}
}
inline ull calc(int mid) {
int len=0,a[20];
while(mid) {
a[++len]=mid%10;
mid/=10;
}
ull res=0;
for(int i=1; i<len; i++) {
for (int j=1; j<10; j++) {
res+=dp[i][j];
if(res>=inf) return inf;
}
}
for(int i=1; i<a[len]; i++) {
res+=dp[len][i];
if(res>=inf) return inf;
}
for(int i=len-1; i>=1; i--) {
ull tmp=0,x=a[i]*pw[i-1];
for(int j=len; j>i; j--) {
tmp=tmp*10+a[j];
}
tmp=tmp*pw[i]-pw[len-1]+1;
if(a[i]==0) tmp=0;
tmp=(tmp+tmp+x-1)*x/2;
res+=tmp;
if(res>=inf) return inf;
for (int j=len-1; j>=1; j--) {
tmp+=(pw[j]-pw[j-1])*x;
res+=tmp;
if(res>=inf) return inf;
}
}
return res;
}
inline int find(ull t,ull x) {
while(1) {
x--;
if(x==0) return t%10;
t/=10;
}
}
inline int solve(int r,ull x) {
for(int i=1; ; i++) {
ull tmp=i*(pw[i]-pw[i-1]);
if(tmp<x) x-=tmp;
else {
int t=pw[i-1]+(x-1)/i;
int p=(x-1)%i+1;
return find(t,i-p+1);
}
}
}
int main() {
freopen("sequence.in","r",stdin);
freopen("sequence.out","w",stdout);
int q,n;
init();
scanf("%d",&q);
while(q--) {
ull x;
cin >> x;
int l=1,r=1e9;
while(l<r) {
int mid=(l+r)/2;
if(calc(mid+1)>=x) r=mid;
else l=mid+1;
}
x-=calc(r);
printf("%d\n",solve(r,x));
}
return 0;
}
4 游戏
(game.cpp/c/pas)
【问题描述】
PYWBKTDA 很喜欢玩一些游戏,比如算 24 点。他最近迷上了一款弹珠游戏。
有 n 个弹珠排成一排,第 i 个弹珠的颜色是 ai。 PYWBKTDA 喜欢把这些弹珠重新排列,使得相同颜色弹珠都在同一个连续区间。也就是说,如果 PYWBKTDA 想要把颜色 i 的全部放在一起,那么颜色 i 弹珠的最左边的位置是 l, 最右边的位置是 r, 那么所有颜色为 i 的弹珠的位置只能在 l 到 r 之间, l 到 r 范围内也只能有颜色 i 的弹珠。
为了达到这个目的, PYWBKTDA 将进行如下操作:找到两个相邻的弹珠,把它们交换一下位置。
现在你的任务是来计算最小的交换次数,使得相同颜色弹珠都在同一区间,在这个区间内,同种颜色的弹珠的位置是无关紧要的。
【输入格式】
输入的第一行是一个整数 n, 表示弹珠的数量。
输入的第二行有 n 个整数 ai, 表示第 i 个位置上弹珠的颜色。
【输出格式】
输出只有一个整数, 表示最少的交换次数。
【输入输出样例 1】
7
3 4 2 3 4 2 2
3
在颜色序列 [3, 4, 2, 3, 4, 2, 2] 中,我们可以把第 3 个和第 4 个交换一下,然后把第 2 个和第 3 个交换一下, 最后把第 4 个和第 4 个交换一下。
【输入输出样例 2】
5
20 1 14 10 2
0
这里不需要交换。
【输入输出样例 3】
13
5 5 4 4 3 5 7 6 5 4 4 6 5
21
【数据范围】
对于 20% 的数据,
1
≤
n
≤
100
,
1
≤
a
i
≤
2
1 ≤ n ≤ 100, 1 ≤ a_i ≤ 2
1≤n≤100,1≤ai≤2。
对于 60% 的数据,
1
≤
n
≤
100000
,
1
≤
a
i
≤
10
1 ≤ n ≤ 100000, 1 ≤ a_i ≤ 10
1≤n≤100000,1≤ai≤10。
对于 100% 的数据,
1
≤
n
≤
100000
,
1
≤
a
i
≤
20
1 ≤ n ≤ 100000, 1 ≤ a_i ≤ 20
1≤n≤100000,1≤ai≤20。
【解析】
20%做法 暴力
60%做法 枚举全排列+逆序对统计
100%做法 状压dp+逆序对统计
众所周知,把一个序列变为不降所需要的相邻元素交换次数就是这个序列的逆序对数。
那么我们可以对每个颜色设置一个大小顺序,比如样例 中 ,答案就是设置大小顺序后的
最小逆序对个数。
颜色数最多 20,我们可以使用状压 dp。
设 f[s] 为已选的数的状态集合s (每选一个数,意味这给其赋一个比之前选的数都大的值),集合中
的数的逆序对的最小值。
我们先 O(20n) 预处理出 w[u][v] 表示有多少对(l,r) 满足l<r 且a[l]=u,a[r]=v 。
转移:设 u 为要加入的颜色,则
f
[
i
]
=
m
i
n
(
f
[
i
]
,
f
[
j
]
+
∑
v
∈
j
w
[
u
]
[
v
]
f[i]=min(f[i],f[j]+\sum_{v \in j} w[u][v]
f[i]=min(f[i],f[j]+∑v∈jw[u][v]
设颜色种数为 c,则时间复杂度O(nc+c22c)。
【代码】
#include <bits/stdc++.h>
using namespace std;
#define ll long long
int const N=400000+5;
ll const inf=1LL<<50;
int n;
int a[N],num[21];
ll sum;
ll cnt[21][21],f[1<<20],vis[20],id[20];
int main() {
freopen("game.in","r",stdin);
freopen("game.out","w",stdout);
scanf("%d",&n);
for(int i=1; i<=n; i++){
scanf("%d",&a[i]);
a[i]--;
vis[a[i]]=1;
}
for(int i=0; i<20; i++)
if(vis[i]) id[i]=sum++;
for(int i=n; i>=1; i--) {
a[i]=id[a[i]];
for(int j=0; j<20; j++) {
cnt[a[i]][j]+=num[j];
}
num[a[i]]++;
}
for(int i=1; i<(1<<sum); i++) f[i]=inf;
for(int i=0; i<(1<<sum); i++) {
for(int j=0; j<sum; j++) {
if(i&(1<<j)) continue;
ll tmp=0;
for(int k=0; k<sum; k++) {
if(!(i&(1<<k))) continue;
tmp+=cnt[j][k];
}
f[i|(1<<j)]=min(f[i|(1<<j)],f[i]+tmp);
}
}
cout << f[(1<<sum)-1] << endl;
return 0;
}