2021年合肥市信息学市赛初中组-T1-小 C 的计算(sum)
题目描述
小 C 擅长计算,整天都在进行着各种各样的计算。
这不,小 C 又开始了一个计算问题:输入两个数 L、R,输出所有 L 到 R 之间(包括 L、R)的质数的和。
输入格式
从文件 sum.in 中读取数据。
一行两个整数 L、R
输出格式
输出到文件 sum.out 中。
一个数
输入输出样例
输入样例1:
15 23
输出样例1:
59
输入样例2:
123456789 123457789
输出样例2:
5925949806
说明
对于 30%的数据,1 ≤ L ≤ R ≤ 1000
对于 100%的数据,1 ≤ L ≤ R ≤ 10^9,R - L ≤ 1000
耗时限制1000ms 内存限制128MB
解析
考点:数学,素数筛
观察数据范围,用素数筛有超时和超内存的风险。数据范围中提到 R-L<= 1000,
因此可以用普通的试除法判断素数,时间复杂度为 O((L-R)*R),能拿 60 分。
试除法+根号优化,拿 100 分。
时间复杂度(R-L)*sqrt(R) 1*10^3+3*10^4 =3*10^7 枚举即可
参考代码
#include<iostream>
#include<cstdio>
using namespace std;
bool is_prime(int x){
if(x<2) return false;
for(int i=2;i*i<=x;i++){
if(x%i==0) return false;
}
return true;
}
int main(){
int L,R;
cin>>L>>R;
long long ans=0;
for(int i=L;i<=R;i++){
if(is_prime(i)) ans+=i;
}
cout<<ans;
return 0;
}
2021年合肥市信息学市赛初中组-T2-小 C 的工作(work)
题目描述
小 C 不喜欢上班。他的老板又给小 C 安排了 n 项任务。老板担心小 C 在公司里不干活儿,于是给每一项任务安排了一个最迟动工时间 ti ,当超过时间 ti 时(不包括 ti这个时间点),如果小 C 仍未动工,就会被扣薪。小 C 可以选择在 ti 时刻之前或者恰好在 ti 时刻办这项任务,一旦选择开始办,就必须连续不断、且时长达到 li 才能完成这项任务。
在任意时刻下,小 C 最多只能做一项任务。小 C 很懒,他想合理安排任务顺序,使得开始办第一项任务的时间尽可能地迟,并且不会被扣薪。
请你告诉他最迟的时间。注意开始时间可能为负数。
输入格式
从文件 work.in 中读取数据。
第一行一个正整数 n,表示任务个数;
接下来 n 行,每行两个整数 ti 和 li ,表示每项任务最迟动工时间以及完成任务所需的工作时长。
输出格式
输出到文件 work.out 中。
仅一行一个数,表示最迟的工作时间。
输入输出样例
输入样例1:
2 1 4 2 2
输出样例1:
-1
输入样例2:
5 2 5 3 3 7 4 8 2 10 1
输出样例2:
-4
说明
【样例 1 解释】
按照 2、1 的任务顺序,工作的时间区间为 [-1,1][1,5]。显然开始工作的时间不能迟于时刻 -1。
【样例 2 解释】
按照 2、1、5、4、3 的任务顺序,工作的时间区间为 [-4,-1] [-1,4] [4,5] [5,7] [7,11]。
【数据范围】
对于 10% 的数据:n = 2;
对于 30% 的数据:n ≤ 10;
对于 60% 的数据:n ≤ 5 × 10^3;
对于 100% 的数据:n ≤ 2 × 10^5,0 < li, ti≤ 10^9。
耗时限制1000ms 内存限制512MB
解析
考点:贪心,区间贪心
已知条件和限制:
- 每项任务有最迟开工时间 ti 和完成时间 li;
- 每个时刻可以不干活,如果干活,同时只能干一件。
问,最迟什么时候开始干活。
抽象为问题模型:给定多个区间,每个区间尽量往右放,但是最右放到坐标 ti 处,区间和区间之间不能有重叠,求最左边线段的左端点坐标。
策略:
- 对于每个线段,能往右放,就尽量往右放。
- 因为是尽量往右放,所以我们 先放最右边的,为了达到这个目的,我们需要先对区间进行排序,按右端点尽可能靠右的原则排序,如果右端点一致,那么让左端点靠右优先的原则排序。
- 考虑到多个区间之间不能有重叠,我们一个区间一个区间地放,同时为了保证不重叠,我们定义一个变量保存当前覆盖掉的最左端(当前最迟开工时间)。
- 当所有区间放完后,上面定义的变量就是所有任务的最迟开工时间。
时间复杂度:O(nlogn)
参考代码
#include<iostream>
#include<algorithm>
#include<cstdio>
using namespace std;
const int N=200010;
struct work{
long long begin,end,len;
}a[N];
//按照结束时间从大到小排序,如果结束时间一致,那么让开始时间靠后的优先放
bool cmp(work x,work y){
if(x.end==y.end){
return x.begin>y.begin;
}
return x.end>y.end;
}
int main(){
int n;
cin>>n;
for(int i=1;i<=n;i++){
cin>>a[i].begin>>a[i].len;
a[i].end=a[i].begin+a[i].len;
}
sort(a+1,a+n+1,cmp);
long long ans=a[1].begin; //记录每次的左端点
for(int i=2;i<=n;i++){
//相交 // 放最右边,会和之前放的有重叠,那么就在之前的基础上,往左紧贴着放
if(ans<a[i].end){
ans-=a[i].len;
}else{ //不相交 // 放最右边,不会重叠,那就放到 a[i].begin 处
ans=a[i].begin;
}
}
cout<<ans;
return 0;
}
2021年合肥市信息学市赛初中组-T3-小 C 爱观察(observe)
题目描述
小 C 非常喜欢树。上次后院的蚂蚁看腻了,这次准备来观察树。
小 C 每天起得早早的,给小树浇水,并且每天记录这棵小树的一些数据。树在小 C 的精心呵护下不断长大。经过若干天的记录,小 C 竟然发现了一棵树生长的规律!
为了阐述其规律,小 C 想先使用一种严谨的语言来抽象化一棵树。
首先,小 C 用图论的概念定义了一棵树T=<V,E>,V 表示所有点构成的集合,E 表示所有边(无向边)构成的集合。一棵具有一定形态的树用一个大写字母简记,一般会使用 T;其大小等于 ∣V∣,即节点的个数。
小 C 发现所有树都有一个共同点:大小为 n 的树,恰好含有 n−1 条边,并且任意两个节点间存在路径使得互相可达。比如说下图中 (A) 是一棵树,而 (B)(C) 却不是。
自然界中所有树都有根,对于树 T 也有且仅有一个根,其为 V 中的某个节点 r。于是 小 C 可以对所有节点定义深度,节点 u 的深度等于 u 到 r 的距离 +1,例如下面这棵树中,令节点 1为根 r,则节点 2、3 的深度为 2,节点 4、5 的深度为 3,而节点 1 自身的深度为 1。
由此可以看出,抽象出来的树和现实中的树正好上下颠倒了。接下来小 C 开始定义生长。某次生长操作用 T=grow(T’,d) 表示,’T’表示生长前的树,T 表示生长之后的树。 成长规律根据参数 d 决定。生长时,’T’中所有深度为 d 的节点同时增加一个新的节点与之连接,得到的树即为 T。比如说下图中 (A) 为原树 T,(B) 为 grow(T,1),(C) 为 grow(T,2)。
小 C 又定义成长,表示一棵树经过一系列生长得到另一棵树的过程。令原树为 T0, 总共 k 次生长操作,第 i 次生长的参数为 di ,则可以表示为:
T1=grow(T0,d1)→T2=grow(T1,d2)→⋅⋅⋅→Tk=grow(Tk−1,dk)
小 C 又定义种子为大小为 11、仅包含根节点的树。下图是一颗种子的成长过程。
然而一个猜想需要诸多事实来支撑。小 C 又观察了许多棵树,然而树儿都长大了,小 C 只能得到成长之后的树 T。他想知道对于一颗种子,存不存在某种成长过程,使得种子 能长成树 T。于是小 C 把问题交给了你。
本题每个输入文件有多组测试数据
输入格式
从文件 observe.in
中读取数据。 第一行一个正整数 Q,表示数据组数。
对于每组数据,将会描述一棵成长之后的树 T;
每组数第一行两个正整数 n 和 r,表示树 T 的大小、T 的根,节点依次从 1 到 n 标号;
接下来 n−1 行,每行两个整数 u 和 v,描述一条边 (u,v)。
保证 T 一定是一棵合法的树。
输出格式
输出到文件 observe.out
中。 总共 Q 行,每行表示对应的树 T 是否存在成长过程,使得种子成长成 T,如果存在, 输出 Yes
,否则输出 No
(请注意大小写)。
样例
输入数据#1
1
6 1
1 2
1 3
2 4
2 5
3 6
输出数据#1
Yes
解释#1
这棵树的形态如下。
此为题面描述的成长过程中的例子。
输入数据#2
1
6 1
1 2
2 3
3 4
1 5
5 6
输出数据#2
No
解释#2
这棵树的形态如下。
一颗种子不存在某种成长方式变成这棵树。
输入数据#3
2
6 1
1 2
1 3
2 4
2 5
3 6
6 1
1 2
2 3
3 4
1 5
5 6
输出数据#3
Yes
No
数据范围
对于 10% 的数据:n≤5。
对于 30% 的数据:n≤10。
对于 50% 的数据:n≤100。
对于 70% 的数据:n≤3×10^3。
对于 100% 的数据:1≤Q≤10,1≤n≤10^5,1≤r≤n。
耗时限制2000ms 内存限制512MB
解析
考点:树结构,搜索,堆
对于给定结构的树,我们可以通过如下方法判断树是否可以“生成”:
从上往下,找到第一个含有叶子的层数,删除当前层的所有叶子。注意,这样可能会导致上一层出现新的叶子。然后再从上往下,找第一个含有叶子的层数,然后做同样的处理,如此反复,直至删除至只有根。在删除过程中,如果发现不满足上述性质的情况,则认为无法生成。
实际实现时,可以先预处理出所有的有叶子的深度,把这些深度用小根堆维护,删除某层叶子时出堆,如果产生新的叶子,把该层入堆。(也可以用单调递增的数组维护,第一个元素即为最浅层,当删除最浅层时,如果不产生新叶子,删除该元素即可,如果产生,必然产生在其父结点,把该元素+1即可)
参考代码
#include <bits/stdc++.h>
using namespace std;
const int N = 1e5+10;
struct node{
vector<int> son;
int p;
int n1,n2; // n1表示孩子结点数,n2表示孩子中的叶子结点数
}trees[N];
vector<int> deeps[N]; // deeps[i]: 保存深度为 i 层的所有非叶子结点编号
int nums[N]; // 标记深度为i的叶子结点数
priority_queue<int,vector<int>,greater<int> > ns; // 小根堆,存有叶子的层次号
void dfs(int u,int d){
for (int i = 0; i < trees[u].son.size(); ++i) {
dfs(trees[u].son[i], d+1);
}
//叶子结点
if(trees[u].son.empty()){
trees[trees[u].p].n2++; // 父结点的叶子结点数++
nums[d]++; // 深度为d的叶子结点数++
if(nums[d] == 1){ // d 层刚有叶子
ns.push(d); // d 层有叶子
}
}else{ // 非叶子结点加入deeps中
deeps[d].push_back(u);
}
}
int main(){
int q;
scanf("%d",&q);
while (q--){
int n,r;
scanf("%d%d",&n,&r);
memset(trees,0, sizeof(trees));
memset(deeps,0, sizeof(deeps));
memset(nums,0, sizeof(nums));
ns = priority_queue<int,vector<int>,greater<int>> ();
int u,v;
for (int i = 1; i <= n - 1 ; ++i) {//n-1条边
scanf("%d%d",&u,&v);
trees[u].son.push_back(v);
trees[v].p = u;
trees[u].n1 ++ ;
}
dfs(r,1);
if(nums[n]){ // 第 n 层有结点,说明是单链,必然可以生成
printf("Yes\n");
continue;
}
while(!ns.empty()){
int d = ns.top(); // 深度最小的叶子结点深度是d,需要从d-1层结点开始删除
int size = deeps[d - 1].size();
if(size > nums[d]){ // 如果上一层结点数比当前层结点数多,根据分析肯定无法生成
printf("No\n");
break;
}
vector<int> newleaf;
int flag = 0;
for (int i = 0; i < deeps[d - 1].size(); ++i) {
int x = deeps[d - 1][i];
if(!trees[x].n2){ // 某个结点没有叶子结点,不满足条件
flag = 1;
break;
}else{
trees[x].n1 --;
trees[x].n2 --;
if(x != r && trees[x].n1 == 0){ // 变成了叶子结点了,先缓存起来,注意根结点就算也不处理
newleaf.push_back(i); // 记录下标,防止遍历复杂度
}
}
}
if(flag){
printf("No\n");
break;
}
if(size == nums[d]){
ns.pop();
}
nums[d] -= size; // 剪去叶子结点
// 处理新产生的叶子结点
for (int i = newleaf.size() - 1; i >= 0 ; i--) {
nums[d-1] ++ ;
if(nums[d-1] == 1){
ns.push(d-1); // 产生了新深度的叶子
}
int t = newleaf[i];
int q = deeps[d-1][t];
trees[trees[q].p].n2 ++ ;
// 对应 deeps 要 删除该结点
deeps[d-1].erase(deeps[d-1].begin() + t);
}
}
if(ns.empty()) printf("Yes\n");
}
return 0;
}
2021年合肥市信息学市赛初中组-T4-小 C 切蛋糕(cake)
题目描述
一年一度的生日又到了。
小 C 准备请他的小L,小 W 等众多朋友们吃蛋糕。
由于预计有 n−2 个人在场,小 C 就买了份正 n 边形的大蛋糕。n 个顶点上均有草莓、 黄桃、葡萄三种水果中的一种。保证蛋糕中三种水果均出现并且任意相邻的两个顶点上水果种类不同。
小 C 现在要切 n−2 块蛋糕,每人一块,他要求:每块蛋糕都是三角形; 各个顶点上都有水果,且包含所有的种类。
在这样苛刻的条件下,小 C 等价于要对蛋糕做一个 n 边形的三角剖分,使得里面的 n−2 个子三角形三个顶点的水果种类各异。请你告诉他一组合法的剖分方式,使得满足小 C 的要求。可以证明:在前面的保证下,必然存在一组解。
n 边形的三角剖分指在 n 边形内部选择 n−3 条对角线连接后,形成的所有 n−2 个三 角形,其顶点均为 n 边形的顶点。显然剖分选取的 n−3 条对角线两两间要么不交,要么仅仅在端点处相交。
本题将使用 Special Judge。只要你的答案合法、正确,将会获得该测试点的全部分数。
输入格式
从文件 cake.in
中读取数据。
共两行,第一行一个正整数 n,表示一个正 n 边形,顶点从 1 到 n 按顺时针依次编号;
第二行有 n 个整数 ai ,其中用数字 1 表示草莓,2 表示黄桃,3 表示葡萄。ai 表示第 i 个顶点上水果的种类。
输出格式
输出到文件 cake.out
中。
共 n−3 行,每行两个数u,v,表示剖分包括边 (u,v)。你需要满足 1≤u,v≤n,且 u≠v,u 和 v 在原多边形上不相邻。
这 n−3 条边描述了一组三角剖分,你需要满足其是一组合法的三角剖分,并且满足所有 n−2 个剖分得到的三角形,三个顶点均含有三种水果,否则会导致答案错误。
样例
输入数据#1
5
1 2 3 2 3
输出数据#1
1 3
1 4
解释#1
如图所示,使用红色表示草莓,黄色表示黄桃,紫色表示葡萄。
输入数据#2
6
1 2 3 1 2 3
输出数据#2
2 4
4 6
6 2
解释#2
如图所示。
方案不唯一。
数据范围
- 对于 10% 的数据:n=4。
- 对于 25%的数据:n≤15。
- 对于另外 5% 的数据:保证只有一个顶点上的水果为草莓。
- 对于 70% 的数据:n≤10^3。
- 对于 100%1 的数据:4≤n≤5×10^5,ai=1,2,3。
校验器
为了方便选手测试,在 cake
目录下我们下发了可执行文件 checker
(无后缀名)(在这里,请用于linux下运行)。 选手可以将输入文件(文件名必须是 cake.in
)和输出文件(文件名必须是 cake.out
)与该可执行文件位于同一目录下,在对应窗口空白处右键“在终端打开”,在出现的终端中 输入“./checker”,运行后会将结果反馈给选手。
反馈的结果有以下可能:
- Correct:答案正确;
- Wrong Answer:答案错误(答案不合题意等);
- Invalid Format:格式不合法(输入不合法、输入输出超出数据范围等);
- Not Found ’cake.in/out’ :未找到输入/输出文件。
耗时限制1000ms 内存限制256MB
解析
考点:计算几何,三角剖分,链表
类三角剖分做法,80分
题目当中有一句很关键的话: 保证蛋糕中三种水果均出现并且任意相邻的两个顶点上水果种类不同。而且,我们也应该能想到: 后面无论如何按何种顺序切,仍能保证任意时刻,任意相邻的两个顶点上水果种类不同。
这个结论可以极大简化我们的代码:
#include<iostream>
using namespace std;
const int N=500010;
int n,m,e[N],ne[N];
struct node{
int u,v;
}ans[N];
int main(){
cin>>n;
for(int i=1;i<=n;i++){
cin>>e[i];
ne[i]=i+1;
}
ne[n]=1; //循环链表
int p=1; //从第一个点开始切,可以是任意点开始
while(n>3){
//查找满足剖分条件的一个顶点p(当前点和其对应的剖分点值相等,就继续找下一个)
while(e[p]==e[ne[ne[p]]]) p=ne[p];
//将p点和p对应三角剖分点 划线
ans[++m]={p,ne[ne[p]]};
//删除剖分三角形中的一个顶点(中间的点)
ne[p]=ne[ne[p]];
//总边数-1
n--;
}
for(int i=1;i<=m;i++){
printf("%d %d\n",ans[i].u,ans[i].v);
}
return 0;
}
上一种写法没有考虑节点颜色个数只有1个的情况:
比如样例1,红色点只有一个,那么每次切分点都必须要有这个红色点,否则就切不出来符合要求的三角形了,另外这样的点也不能被删除。
例如我们从5号节点开始剖分,如果5和2连接,那么1就被删除,那么剩下的点无论怎么剖分
都不能符合要求(没有红色点了),对应的边就不会减少,就会无限循环下去。
参考代码:
#include<iostream>
using namespace std;
const int N=500010;
int n,m,e[N],ne[N],b[5];
struct node{
int u,v;
}ans[N];
int main(){
cin>>n;
for(int i=1;i<=n;i++){
cin>>e[i];
b[e[i]]++; //记录点值出现的次数
ne[i]=i+1;
}
ne[n]=1; //循环链表
int p=1; //从第一个点开始切,可以是任意点开始
while(n>3){
//查找满足剖分条件的一个顶点p(当前点和其对应的剖分点值相等,就继续找下一个)
//如果剖分三角中间的点点值为1,也需要跳过,否则被删除后,会死循环,
//当一个点的点值为1时,这个点必须要要选,这样才能构成符合要求的三角形
//颜色相同或者只剩下一种颜色(不能切),则接着往后找
while(e[p]==e[ne[ne[p]]]||b[e[ne[p]]]==1) p=ne[p];
//将p点和p对应三角剖分点 划线
ans[++m]={p,ne[ne[p]]};
//删除点的点值-1
b[e[ne[p]]]--;
//删除剖分三角形中的一个顶点(中间的点) // 删掉 p 后面的节点:切一刀,把 nxt[p] 这个角切掉
ne[p]=ne[ne[p]];
//总边数-1
n--;
}
for(int i=1;i<=m;i++){
printf("%d %d\n",ans[i].u,ans[i].v);
}
return 0;
}
三角剖分
对于一个简单多边形,可以连接各个节点,然后将原图形分割成若干个三角形。关于三角剖分还有如下一些概念:
-
耳朵:多边形的一个点,它与它相邻的两个点构成的三角形完全包含在多边形内,这个三角形称为该多边形的耳朵(ear)
-
嘴巴:如果多边形上一点与它相邻的两个点构成的三角形完全在多边形外,称为嘴巴(mouth)
三角剖分的思路很简单:不断地切去多边形的一个耳朵,直至它只剩下一个三角形,那么就完成了该多边形的三角剖分。
实现起来是这样的:
- 初始化:首先找到一个凸点J(Lowest-then-Leftmost原则),然后找到与它相邻的两个点I,K并把I,K连接起来。
- 递归基:最终变成一个三角形(无洞)
- 递归假设:每次都将多边形分割成更小的简单多边形
- 递归:如果△IJK是一个耳朵,那么IK是一条内对角线,可切。如果IK不是内对角线,意味着中间有空洞。假设M是离线IK最远的那个点。
本题多了一个限制:剖分的三角形三个顶点数字不能相同。那么初始化就要去找第一个满足条件的顶点。然后再递归分割下去。
参考代码:
#include <bits/stdc++.h>
using namespace std;
const int N = 5e5+5;
int n, l[N], r[N], c[N];
int ans1[N], ans2[N], len, s[4];
void init(){
memset(s, 0, sizeof s);
for(int i = 1; i <= n; i++) {
s[c[i]] ++;
l[i] = i-1; r[i] = i+1;
}
l[1] = n; r[n] = 1;
}
bool check(int p){
if(c[p]+c[r[p]]+c[r[r[p]]] == 6){ // 可以切
ans1[++len] = p; ans2[len] = r[r[p]]; // 保存方案
s[c[r[p]]]--; // 切掉 r[p] 节点
int d = r[p];
r[l[d]] = r[d];
l[r[d]] = l[d];
return true;
}
return false;
}
int main(){
cin >> n;
for(int i = 1; i <= n; i++){
cin >> c[i];
}
init();
int pp = 1; // 找到第一刀可以切的位置——“耳朵”
while(!(c[pp]+c[r[pp]]+c[r[r[pp]]] == 6)) {
pp = r[pp];
}
int flag = 0;
for(int x = pp; x < pp+3; x++) { // 最多三种起始切法,逐一尝试
init();
int p = x, cut = 0;
flag = 1;
len = 0;
while(cut < n-3) {
int start = p;
while(!(c[p]+c[r[p]]+c[r[r[p]]] == 6)) { // 找到第一个“耳朵”
p = r[p];
if(p == start) break;
}
if(p == start && !(c[p]+c[r[p]]+c[r[r[p]]] == 6)){
flag = 0; // 当前方案无法找到,直接结束当前尝试
break;
}
while(cut < n-3&&check(p)) {
// 时间效率优化:如果当前节点不是最少节点,继续往右尝试,否则仍判断当前节点
if(s[c[p]] != min(s[1],min(s[2],s[3])))
p = r[p];
cut++;
}
p = r[p];
}
if(flag) { // 方案可行,输出答案
for(int i = 1; i <= len; i++)
cout << ans1[i] << ' ' << ans2[i] << '\n';
break;
}
}
if(!flag) {
cout << "Not found\n"; // 测试用
}
return 0;
}
本年一等190,二等120。