2022“杭电杯”中国大学生算法设计超级联赛(3)
1001 | 1002 | 1003 | 1004 | 1005 | 1006 | 1007 | 1008 | 1009 | 1010 | 1011 | 1012 | |
赛时过题 | O | O | O | O | ||||||||
赛后补题 | 待补 | 待补 | O |
赛后总结:
悲。。。怎么都做不出来1011,杭电的题好像很喜欢卡时限,5e5的数据我跑O(nlogn2n)的做法直接T傻了,如果是1e5我还能跑一跑。
比赛时换了3种写法写1011,可惜都是nlog2n做法,全都超时了。
今天有把考场上10人以上过的题都看了,不过还是成效不好。。。
所以说在写一道码量超大的题之前一定要把所有细节全部考虑好了再下手!即写个伪代码,理清思路同时计算好时间复杂度再动手!
如果我算了一下发现时间复杂度不对我就不会去写(注意如果有一道题忽然有很多人过了那大概说明写法很简单或者数据很水),我就有时间钻研1001,1008。。
总的来说,下次算复杂度时一定要算准确一点,比如说1011我的做法看似是knlognlogm,但由于k=8,n=5e5,logn=20,logm=30,实际上总共运算次数为24e8,再算上其他一些开销就肯定过不了了,除非忽然很多人过才考虑暴数据很水复杂度不会跑满。
另外开题不仅仅要看题,最好要稍微钻研一下。。。我发现1011碰壁了想不出nlogn做法就不要去做了,赶紧去做1008这题,这题赛场上是很有希望做出来的!
1008赛场上是100+人过,即使1011是200+人过,但100+人和200+人对于我们来说难度是一样的,不要舍不得放下1011,赶紧看其他题!
赛时排名:
5题末尾:182名
6题末尾:126名
7题末尾:80名
8题末尾:38名
1003 Cyber Language
题目难度:check-in
题目大意:如果一个字符是小写字母且前一个字符是空格或者它是第一个字符,那么把它转大写输出。
解题思路:模拟,需要注意读入一行后吞回车。
参考代码:
查看代码
#include<iostream>
#include<cstdio>
char s[10000];
int main()
{
int T;scanf("%d",&T);scanf("%[^\r\n]",s);getchar();
while(T--)
{
scanf("%[^\r\n]",s);getchar();
for(int i=0;s[i];i++) if (i==0||s[i-1]==' ') putchar(s[i]-'a'+'A');putchar('\n');
}
return 0;
}
1009 Package Delivery
题目难度:easy
题目大意:有n个快递,每个快递有开始日期l和截止日期r。每个日期可以拿若干次(>=0)快递,每次最多拿k个快递。问最少拿几次快递可以把所以快递拿完,T组数据。
数据范围:1<=T<=3000,1<=k<=n<=1e5,∑n<=1e6
解题思路:贪心,可以发现每次要尽可能拿k个快递,那么每次拿快递都越晚拿越好,就会有更多的快递可拿。
有一个很直观的想法,如果一个快递到了截止日期了那么就必须拿了,同时还可以顺带拿k-1个快递。
拿完k个快递后,为了下次拿快递也能尽可能拿足k个,下次拿快递越晚越好。
因此顺带拿的那k-1个快递应该要是截止日期最早的那k-1个。
因此把所有快递拆出两个端点排序,然后模拟时间流动,设当前是第x天。
先加入所有今天开始的快递,然后处理所有今天截止的快递(如果该快递已经在前几天被拿了那就跳过),并顺带取掉截止日期最小的k-1个快递。
参考代码:
查看代码
#include<iostream>
#include<cstdio>
#include<algorithm>
#include<vector>
#include<queue>
#define For(i,a,b) for(int i=a;i<=b;i++)
const int N=1e5+1000;
struct Package
{
int l,r;
}s[N];
struct DATA
{
int key,type,id;
};std::vector<DATA> data;
int cmp(const DATA&a,const DATA&b)
{
if (a.key!=b.key) return a.key<b.key;
return a.type<b.type;
}
std::priority_queue<std::pair<int,int> > minr;
int vis[N];
int main()
{
int T;scanf("%d",&T);
while (T--)
{
int n,k;scanf("%d%d",&n,&k);data.clear();
For(i,1,n)
{
scanf("%d%d",&s[i].l,&s[i].r);vis[i]=0;
data.push_back((DATA){s[i].l,1,i});
data.push_back((DATA){s[i].r,2,i});
}
std::sort(data.begin(),data.end(),cmp);
int ans=0;while (!minr.empty()) minr.pop();
For(i,0,(int)data.size()-1)
{
if (data[i].type==1) minr.push(std::make_pair(-s[data[i].id].r,data[i].id));
else
{
if (vis[data[i].id]) continue;
ans++;
For(j,1,k)
{
if (minr.empty()) break;
int u=minr.top().second;
vis[u]=1;minr.pop();
}
}
}
printf("%d\n",ans);
}
return 0;
}
1012 Two Permutations
题目难度:medium-easy
题目大意:有两个长度为n的排列P、Q,P和Q相互穿插合并为数列R(P为R的子序列,R的另一部分为Q)。给定P,Q,R,问有几种方案使P、Q合并为R。
数据范围:1<=T<=300,1<=n<=3e5,∑n<=2e6
解题思路:
先考虑从R确定哪一部分属于P,那么另一部分就属于Q,假设当前处理到R的第i位,如果这一位对应P中的第j个元素,那么P中的前j个元素一定是R的前i个元素的子序列,那么剩下的那部分就必定要和Q匹配。
感觉用R来作为dp状态不太方便,考虑用P来作为dp状态,即:
dp[i][j]表示P中的第i个元素与R的第j个位置相匹配的方案数。
更进一步,由于P的第i个元素最多和R有两个位置匹配,记dp[i][0..1]表示P中的第i个元素与R中该元素出现的第一个/第二个位置匹配的方案数。
那么dp[i][0]=dp[i-1][0]*[ P的第i-1个元素匹配R中的第一个位置, P的第i个元素匹配R中的第一个位置,这两个位置之间的那部分元素和Q的那部分元素一一匹配 ]+
dp[i-1][1]*[P的第i-1个元素匹配R中的第二个位置, P的第i个元素匹配R中的第一个位置,这两个位置之间的那部分元素和Q的那部分元素一一匹配 ]
dp[i][1]=dp[i-1][0]*[ P的第i-1个元素匹配R中的第一个位置, P的第i个元素匹配R中的第二个位置,这两个位置之间的那部分元素和Q的那部分元素一一匹配 ]+
dp[i-1][1]*[P的第i-1个元素匹配R中的第二个位置, P的第i个元素匹配R中的第二个位置,这两个位置之间的那部分元素和Q的那部分元素一一匹配 ]
其实标答这部分是用字符串哈希O(1)做的,但是我直接暴力匹配也能卡着时限(2s/ 4s)过hhh,可能是因为暴力匹配的复杂度近似均摊O(n)吧
赛时经历:
这道题我们没沟通好,一开始大家都没思路,然后有一点思路后都说如果你们没思路那我先写,结果写了3份代码。。。
应该分出一个人看其他题的emmm
还好我一发过了才没罚时
参考代码:
查看代码
#include<iostream>
#include<cstdio>
#define For(i,a,b) for(int i=a;i<=b;i++)
const int N=3e5+1000;
const long long mod=998244353;
int n,first[N],second[N],P[N],Q[N],S[2*N],cnt[N];
long long dp[N][2];
int judge(int l,int r,int a,int b)
{
// printf("!! %d %d %d %d\n",l,r,a,b);
if (l>r&&a>b) return 1;
if (l>r||a>b) return 0;
if (r-l!=b-a) return 0;
int len=r-l;
For(i,0,len)
{
if (S[l+i]!=Q[a+i]) return 0;
}
return 1;
}
int main()
{
int T;scanf("%d",&T);
while (T--)
{
scanf("%d",&n);For(i,1,n) first[i]=second[i]=cnt[i]=0;
For(i,1,n) scanf("%d",&P[i]);
For(i,1,n) scanf("%d",&Q[i]);
For(i,1,2*n)
{
scanf("%d",&S[i]);cnt[S[i]]++;
if (first[S[i]]) second[S[i]]=i;
else first[S[i]]=i;
}
int flag=1;
For(i,1,n) if (cnt[i]!=2)
{
printf("%d\n",0);
flag=0;break;
}if (!flag) continue;
dp[1][0]=judge(1,first[P[1]]-1,1,first[P[1]]-1);
dp[1][1]=judge(1,second[P[1]]-1,1,second[P[1]]-1);
For(i,2,n)
{
dp[i][0]=0;
if (first[P[i-1]]<first[P[i]]&&judge(first[P[i-1]]+1,first[P[i]]-1,first[P[i-1]]-(i-1)+1,first[P[i]]-i))
dp[i][0]=(dp[i][0]+dp[i-1][0])%mod;
if (second[P[i-1]]<first[P[i]]&&judge(second[P[i-1]]+1,first[P[i]]-1,second[P[i-1]]-(i-1)+1,first[P[i]]-i))
dp[i][0]=(dp[i][0]+dp[i-1][1])%mod;
dp[i][1]=0;
if (first[P[i-1]]<second[P[i]]&&judge(first[P[i-1]]+1,second[P[i]]-1,first[P[i-1]]-(i-1)+1,second[P[i]]-i))
dp[i][1]=(dp[i][1]+dp[i-1][0])%mod;
if (second[P[i-1]]<second[P[i]]&&judge(second[P[i-1]]+1,second[P[i]]-1,second[P[i-1]]-(i-1)+1,second[P[i]]-i))
dp[i][1]=(dp[i][1]+dp[i-1][1])%mod;
}
// For(i,1,n) printf("?? %d %d %lld %lld\n",first[P[i]],second[P[i]],dp[i][0],dp[i][1]);
long long ans=0;
if (judge(first[P[n]]+1,2*n,first[P[n]]-n+1,n)) ans=(ans+dp[n][0])%mod;
if (judge(second[P[n]]+1,2*n,second[P[n]]-n+1,n)) ans=(ans+dp[n][1])%mod;
printf("%lld\n",ans);
}
return 0;
}
1011 Taxi
题目难度:medium
题目大意:平面上有n个点(xi,yi),每个点有个点权wi。q次询问,每次询问给出一个坐标(x',y'),求max( min(wi,|xi-x'|+|yi-y'|) )
赛时经历:
这题是丁老师开的,以下为丁老师思路:
先转换坐标,变成求max( min(wi,max(|xi-x'|,|yi-y'|) ) )
假设答案是mid,那么max( min(wi,max(|xi-x'|,|yi-y'|) ) )>=mid
即 有一个点i min(wi,max(|xi-x'|,|yi-y'|) ) >=mid
即 有一个点i wi>=mid且 max(|xi-x'|,|yi-y'|)>=mid
由于max(|xi-x'|,|yi-y'|)>=mid
<=> |xi-x'| >mid 或 |yi-y'|>mid
<=> xi-x'>mid 或 x'-xi>mid 或 yi-y'>mid 或 y'-yi>mid
<=> xi>x'+mid 或 xi<x'-mid 或 yi>y'+mid 或 yi<y'-mid
那么对这四部分分别查询是否有一个点满足wi>=mid即可
第一次写:用fhq-treap写,对x轴和y轴分别建一个treap,treap存pair<坐标,w>。结果T了。。。对x,y分别查询,每次查询要2个split和2个merge,复杂度=O(8nlog2n)=24e8...满数据30s
第二次写:用树状数组写,对x轴和y轴分别建一个前缀&后缀最大值树状数组。结果又T了。。。对x,y分别查询,每次查询要先lower_bound查询离散化的2个坐标再问前后缀,复杂度=O(8nlog2n)=24e8。。。满数据跑了30s
第三次:直接用前后缀最大值写,结果又T了。。。对x,y分别查询,每次查询要先lower_bound查询离散化的2个坐标再问前后缀,复杂度=O(4nlog2n)=12e8。。。满数据还是跑了30s?不知道是不是vector的lower_bound比较慢?
第四次写(赛后):实际上每个点i对周围点的贡献是:如果距离d<wi,那么贡献是d,否则贡献是wi。那么就可以根据wi框出一个正方形,正方形内的点贡献为d,外的点贡献为wi
用扫描线来写,如果线段树内用单点修改存点的y坐标,无法保证该点的有效范围是wi,如果线段树采用区间覆盖存这个2w区域内的最小/最大坐标,在扫描线扫过一个矩形后需要撤销时很难做,一个区间如果撤销某个点那它的最大/最小值应该变成什么?考虑线段树每个节点存一个set,存这个区间进行了哪些修改,撤回时erase即可。
结果复杂度还是O(nlog2n)。。。满数据一跑还是30s。。。裂开
解题思路一:
首先转换坐标,则d=max(|xi-x'|,|yi-y'|) ,如果没有w的限制那么如果想要求(x',y')在一部分点集S中的最大距离则可以通过S内xy坐标的最大最小值O(1)求出(x',y')对S中所有点的最大距离d。
再考虑加上w的限制,没什么思路就猜测把所有点按照w从小到大排序,再将所有点分成两部分考虑:
①如果min(w)>=d,也就是说d一定可以取到,那么这部分点对答案的贡献就是d,再考虑另一部分点。
②如果min(w)<d,也就是说d不一定可以取到,设从点p得到的d,如果wp<d那么该点贡献为wp,如果wp>=d那该点贡献为d。又wp>=min(w),d>min(w),则这部分点对答案的贡献最起码是min(w)。
另外可以发现某部分点对答案的贡献一定<=max(w)
(对于每个点min(w,d)<=w且min(w,d)<=d ,则min(w,d)<=max(w)且min(w,d)<=max(d) ,则 min(w,d)<=min(max(w),max(d) )
也就是说另一部分的点如果max(w)<=该部分的min(w),就不需要考虑了。
换句话说,如果后面那部分的min(w)<d,则前一部分的所有点都不需要考虑了。
综上,只需要维护任意区间内的xy坐标最大最小值然后二分即可,可以用ST表实现。
参考代码:1840ms/2000ms
查看代码
#include<iostream>
#include<cstdio>
#include<algorithm>
#define For(i,a,b) for(int i=a;i<=b;i++)
#define Frd(i,a,b) for(int i=a;i>=b;i--)
const int N=1e5+1000;
const int INF=0x3f3f3f3f;
const int Logn=25;
struct DATA
{
int x,y,w;
} s[N];
int xmin[N][Logn],xmax[N][Logn],ymin[N][Logn],ymax[N][Logn],match[N];
int cmp(const DATA&a,const DATA&b){return a.w<b.w;}
int main()
{
int T;scanf("%d",&T);
while (T--)
{
int n,q;scanf("%d%d",&n,&q);
For(i,1,n) {long long tx,ty;scanf("%lld%lld%lld",&tx,&ty,&s[i].w);s[i].x=tx-ty;s[i].y=tx+ty;}
std::sort(s+1,s+1+n,cmp);
For(i,1,n) xmin[i][0]=xmax[i][0]=s[i].x,ymin[i][0]=ymax[i][0]=s[i].y;
For(i,0,20)
{
int st=1<<i,ed=(1<<(i+1))-1;
For(j,st,ed)
{
if (j>n) break;
match[j]=i;
}
}
For(i,1,20) For(j,1,n)
{
int delta=1<<(i-1);
xmin[j][i]=xmin[j][i-1];
xmax[j][i]=xmax[j][i-1];
ymin[j][i]=ymin[j][i-1];
ymax[j][i]=ymax[j][i-1];
if (j+delta<=n)
{
xmin[j][i]=std::min(xmin[j][i],xmin[j+delta][i-1]);
xmax[j][i]=std::max(xmax[j][i],xmax[j+delta][i-1]);
ymin[j][i]=std::min(ymin[j][i],ymin[j+delta][i-1]);
ymax[j][i]=std::max(ymax[j][i],ymax[j+delta][i-1]);
}
}
For(i,1,q)
{
int tx,ty,x,y;scanf("%d%d",&tx,&ty);x=tx-ty;y=tx+ty;
int l=1,r=n,ans=0;
while (l<r)
{
int mid=(l+r)>>1;int len=match[r-(mid+1)+1];
int nowxmin= std::min(xmin[mid+1][len],xmin[r-(1<<len)+1][len]);
int nowxmax= std::max(xmax[mid+1][len],xmax[r-(1<<len)+1][len]);
int nowymin= std::min(ymin[mid+1][len],ymin[r-(1<<len)+1][len]);
int nowymax= std::max(ymax[mid+1][len],ymax[r-(1<<len)+1][len]);
int d=std::max(std::max(x-nowxmin,nowxmax-x),std::max(y-nowymin,nowymax-y));
if (d<=s[mid+1].w)
{
ans=std::max(ans,d);
r=mid;
}
else
{
l=mid+1;
}
}
int d=std::max(std::max(x-s[l].x,s[l].x-x),std::max(y-s[l].y,s[l].y-y));
ans=std::max(ans,std::min(s[l].w,d));
printf("%d\n",ans);
}
}
return 0;
}
解题思路二:
思路类似思路一,不过考虑的区间不一样,思路一是不断缩小可更新答案的区间,思路二是二分可更新答案的区间的左端点(固定右端点)。
把所有点按照w从小到大排序,再求出xy坐标的后缀最大最小值用于求d的后缀最大值。
可以注意到w是不下降的,d是不上升的。
取区间[l,last]中的点进行考虑:设l从后往前移
如果l在交点之后,那么该区间对答案的贡献一定是max(d),随着l的减小max(d)逐渐增大。
如果l在交点之前,如果新加入的点p wp<=dp,则这个点的贡献为wp;如果wp>dp,则这个点的贡献为dp,又由于加入当前之前的max(d)对应的那个点的min(w,d)满足d>=wp,w>=wp,故即使认为该点对答案的贡献为wp也不会影响答案正确性。
那么就可以认为答案是红线那部分。
因此二分交点,即第一个max(d)<=w的位置,如果是交点就取它,如果没有交点则取max(该点,它前面的的个点)作为答案。
参考代码:951ms/2000ms
查看代码
#include<iostream>
#include<cstdio>
#include<algorithm>
#define For(i,a,b) for(int i=a;i<=b;i++)
#define Frd(i,a,b) for(int i=a;i>=b;i--)
const int N=1e5+1000;
const int INF=0x3f3f3f3f;
struct DATA
{
int x,y,w;
} s[N];
int xmin[N],xmax[N],ymin[N],ymax[N];
int cmp(const DATA&a,const DATA&b){return a.w<b.w;}
int main()
{
int T;scanf("%d",&T);
while (T--)
{
int n,q;scanf("%d%d",&n,&q);
For(i,1,n) {long long tx,ty;scanf("%lld%lld%lld",&tx,&ty,&s[i].w);s[i].x=tx-ty;s[i].y=tx+ty;}
std::sort(s+1,s+1+n,cmp);
xmin[n+1]=ymin[n+1]=INF;
xmax[n+1]=ymax[n+1]=-INF;
Frd(i,n,1)
{
xmin[i]=std::min(xmin[i+1],s[i].x);
xmax[i]=std::max(xmax[i+1],s[i].x);
ymin[i]=std::min(ymin[i+1],s[i].y);
ymax[i]=std::max(ymax[i+1],s[i].y);
}
For(i,1,q)
{
int tx,ty,x,y;scanf("%d%d",&tx,&ty);
x=tx-ty;y=tx+ty;
int l=1,r=n+1,ans=0;
while (l<r)
{
int mid=(l+r)>>1;
int d=std::max(std::max(x-xmin[mid],xmax[mid]-x),std::max(y-ymin[mid],ymax[mid]-y));
if ( d>s[mid].w) l=mid+1;
else r=mid;
}
if (l<=n) ans=std::max(ans,std::max(std::max(x-xmin[l],xmax[l]-x),std::max(y-ymin[l],ymax[l]-y)));
if (l-1>=1) ans=std::max(ans,s[l-1].w);
printf("%d\n",ans);
}
}
return 0;
}
1002 Boss Rush
题目难度:medium
题目大意:打血量为H的boss,有n个技能,每个技能有一个释放时间t和伤害持续时间len,在释放时间内不可释放其他技能,在伤害持续时间内的不同时刻会产生不同的伤害d。每个技能只能释放一次,问最早什么时候boss被打败?
数据范围:1<=T<=100,1<=n<=18,1<=H<=1e18,1<=t,len<=1e5(n>10的数据最多5组)
解题分析:
首先发现n很小,猜测与状态压缩dp有关。
然后假设怪物在x时刻被打败,那么如何最大化所有技能在x时刻之前的伤害?
考虑状态压缩dp,设dp[state]为这些技能在x时刻之前的最大总伤害,那么可以枚举最后一个释放的技能,那么无论其他技能的释放顺序如何,那些技能的释放时间之和是可以确定的,也就是说最后一个释放的技能的释放时间是可以确定的,也就是说最后一个释放的技能在x时刻之前的总伤害是可以确定的。
如果某个dp值>=H,就说明这个x是可行的,否则不可行。
复杂度O(log(ans) * 2n *n) 。
赛时经历:这题是丁老师和宇彬做的,我全程没参与。。。
参考代码:
查看代码
#include<iostream>
#include<cstdio>
#define For(i,a,b) for(int i=a;i<=b;i++)
const int N=20;
const int M=1e5+1000;
long long n,H;
struct DATA
{
long long t,len;
long long sum[M];
}s[N];
std::pair<long long,long long> dp[1<<N];
int check(long long deadtime)
{
dp[0].first=dp[0].second=0;
For(i,1,(1<<n)-1)
{
dp[i].first=dp[i].second=0;
For(j,1,n)
{
int k=1<<(j-1);
if (!(i&k)) continue;//补题的时候把i&k 写成j&k了.....比赛的时候每写一个子程序都必须确保这个子程序正确,免去debug的时间
dp[i].second+=s[j].t;
long long sum;
if (deadtime-dp[i-k].second<=0) sum=0;
else if (deadtime-dp[i-k].second<=s[j].len) sum= s[j].sum[deadtime-dp[i-k].second];
else sum=s[j].sum[s[j].len];
dp[i].first=std::max(dp[i].first,dp[i-k].first+sum);
}
if (dp[i].first>=H) return 1;
}
return 0;
}
int main()
{
int T;scanf("%d",&T);
while (T--)
{
scanf("%lld%lld",&n,&H);
long long tot=0;
For(i,1,n)
{
scanf("%lld%lld",&s[i].t,&s[i].len);
For(j,1,s[i].len)
{
scanf("%lld",&s[i].sum[j]);
s[i].sum[j]+=s[i].sum[j-1];
}
tot+=s[i].sum[s[i].len];
}
if (tot<H) {printf("%d\n",-1);continue;}
long long l=1,r=2e6;
while (l<r)
{
long long mid=(l+r)>>1;
if (check(mid)) r=mid;
else l=mid+1;
}
printf("%lld\n",l-1);
}
return 0;
}
1008
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· DeepSeek 开源周回顾「GitHub 热点速览」
· 物流快递公司核心技术能力-地址解析分单基础技术分享
· .NET 10首个预览版发布:重大改进与新特性概览!
· AI与.NET技术实操系列(二):开始使用ML.NET
· .NET10 - 预览版1新功能体验(一)