2022“杭电杯”中国大学生算法设计超级联赛(8)
1001 | 1002 | 1003 | 1004 | 1005 | 1006 | 1007 | 1008 | 1009 | 1010 | 1011 | 1012 | 1013 | |
赛时过题 | O | O | O | O | O | ||||||||
赛后补题 | O |
赛后总结:
杭电的机子实在是太太太太太太太太太卡常了!!!!!!
1002我的线段树只需要在push_up和push_down的时候加上inline就过了。。。
好吧,这次遇到了,下次一定。
卡常技巧:inline,尽可能少使用vector,循环展开,if(>=mod)-=mod和if(<0)+=mod代替%mod,不会被修改的传参用const&。
有一说一,在发现被卡常的时候应该一边让队友疯狂交,一边尽可能修改代码减小常数。。。而不是光疯狂交不改代码。。。
主要还是因为有一发卡进去了让我有了侥幸心理,ε=(´ο`*)))唉
越想越可惜,只差这一题就可以校队第二了,害
今天这场也让我深刻感受到了,要注意自己的长处和短处,比如我的长处可能是dp和数据结构,短处可能是贪心,一般贪心的题都有很多人过但事实上我不一定能立刻做出来,像今天这个1002赛时只有70个人做出来了,但我也能做出来,这就是我的长处。
赛时排名:
6题末尾:146名
7题末尾:77名
8题末尾:55名
1004 Quel'Thalas
题目难度:check-in
题目大意:有一个正方形,从(0,0)~(n,n)总共有(n+1)2个点。每次可以选择一条直线覆盖该直线上的所有点。
问把除了(0,0)的(n+1)2-1个点全部覆盖,最少要几条直线
解题分析:随便画一下,如果笔直的切,数量是2*n,如果是斜向右下切,数量也是2*n,那么大胆猜测答案是2*n即可。
参考代码:
查看代码
/*#if(__cplusplus == 201103L)
#include <unordered_map>
#include <unordered_set>
#else
#include <tr1/unordered_map>
#include <tr1/unordered_set>
namespace std
{
using std::tr1::unordered_map;
using std::tr1::unordered_set;
}
#endif*/
#include<bits/stdc++.h>
using namespace std;
int n,T;
int main()
{
cin>>T;
while(T--)
{
cin>>n;
cout<<n*2<<endl;
}
return 0;
}
1001 Theramore
题目难度:easy
题目大意:给定一个字符串,每次操作可以选择长度为奇数的区间进行前后翻转,询问能得到的最小字典序的字符串是什么。
赛时经历:
随便模拟了一下,为了让字典序最小,那么当前位肯定要尽可能是0,发现很难贪心,要么尽可能把1往后放,要么尽可能把0往前提,还能发现长度为偶数的区间可以任意移动。
再看一下样例,发现样例的答案都是0000 101010 1111
然后稍微证一下,发现最优的情况一定是0000 101010 1111,没办法更优了
那么考虑维护101010 11111
假设当前已经处理到第i位,前i-1位字符串已经到达最优的0000 101010 1111 形式,那么考虑第i位:
①第i位是'0'
要尽可能把这个'0'往前提,如果101010 1111的长度为偶数,那么直接把这个区间和当前这个0放在一起翻转,形成0 1111 01010101,由于010101长度一定是偶数,偶数区间可以任意移动,那么一定可以形成0 01010101 1111,把前两个0输出出来然后再维护后面的101010 11111即可。
②第i位是'1'
要尽可能把这个'1'往后方,那么发现直接把它放最后是最优的。
标答分析:
发现翻一个长度为奇数(设为k)的区间,能再翻其内部长度为k-2的区间,那么就相当于交换前后两个数。
还能发现任意一种翻区间的方式,都能通过交换前后2个数构造出来。
换而言之,该问题等价于:将奇数位置和偶数位置分别考虑,字典序最小是多少?
那么对奇数位置和偶数位置分别排序即可。
(难怪考场这么多人过,是我想复杂了)
参考代码:
查看代码
#include<iostream>
#include<cstdio>
#include<cstring>
#define For(i,a,b) for(int i=a;i<=b;i++)
const int N=1e5+1000;
char s[N];
int main()
{
int T;scanf("%d",&T);
while (T--)
{
scanf("%s",s+1);
int len=strlen(s+1);
int cnt10=0,cnt1=0;
For(i,1,len)
{
if (s[i]=='0')
{
if (!(cnt1&1))
{
putchar('0');
if (cnt10)
{
putchar('0');
cnt10--;
cnt1++;
}
}
else cnt1--,cnt10++;
}
else cnt1++;
// printf("!! %d %d %d\n",i,cnt10,cnt1);
}
For(i,1,cnt10) putchar('1'),putchar('0');
For(i,1,cnt1) putchar('1');putchar('\n');
}
return 0;
}
1011 Stormwind
题目难度:check-in
题目大意:一个n*m的长方形,可以沿水平或竖直方向画若干条线,每条线的两端点都在长方形的边界上。要求这些线划分出的每个小长方形面积都≥k,求最多可以画几条线。
数据范围:T≤100,n,m,k≤1e5
解题分析:
首先先假设已经画好了所有竖线,那么可以贪心画横线。为了让横线尽可能多,那么每个长方形都要尽可能逼近k,可以发现这个仅和画好竖线后的最短的那一段有关,根据最短的那一段作为宽,可以确定横线最短为多少。
那么就只要枚举画好竖线的最短的那一段的长度,那么竖线可以贪心画,同时横线也可以贪心画。
赛时经历:
这题我赛时没开,是宇彬做的,赛后补题发现是一道签到题,不知道为啥这题赛时会WA两发(
参考代码:
查看代码
#include<iostream>
#include<cstdio>
#define For(i,a,b) for(int i=a;i<=b;i++)
int main()
{
int T;scanf("%d",&T);
while (T--)
{
long long n,m,k;scanf("%lld%lld%lld",&n,&m,&k);
long long ans=0;
For(i,1,m)
{
int j=(k+i-1)/i;if (j>n) continue;
ans=std::max(ans,m/i-1+n/j-1);
// printf("?? %d %lld %d %lld\n",i,m/i-1,j,n/j-1);
}
printf("%lld\n",ans);
}
return 0;
}
1008 Orgrimmar
题目难度:easy
题目大意:求一棵树的最大分离集的大小。最大分离集是一个点集,点集中的每个点最多和一个点联通,即若干孤立点+若干点对。
赛时情况:
一眼树形DP。每个点设三个状态不选、选了但孤立,选了且以匹配为点对,直接DP即可。
纯纯签到题(,不过在代码里修改栈空间还是第一次见
参考代码:
#include<iostream>
#include<cstdio>
#include<vector>
#include<cstring>
#include<cstdlib>
#define For(i,a,b) for(int i=a;i<=b;i++)
const int N=5e5+1000;
const int INF=0x3f3f3f3f;
int dp[N][5];
std::vector<int> To[N];
void dfs(int now,int pa)
{
dp[now][0]=0;
dp[now][1]=1;
int sum=0;
For(i,0,(int)To[now].size()-1)
{
int v=To[now][i];if (v==pa) continue;
dfs(v,now);
dp[now][0]+=std::max(std::max(dp[v][0],dp[v][1]),dp[v][2]);
dp[now][1]+=dp[v][0];
sum+=dp[v][0];
}
dp[now][2]=-INF;
For(i,0,(int)To[now].size()-1)
{
int v=To[now][i];if (v==pa) continue;
dp[now][2]=std::max(dp[now][2],sum-dp[v][0]+dp[v][1]+1);
}
}
void Solve()
{
int T;scanf("%d",&T);
while (T--)
{
int n;scanf("%d",&n);
For(i,1,n) To[i].clear();
For(i,1,n-1)
{
int u,v;scanf("%d%d",&u,&v);
To[u].push_back(v);To[v].push_back(u);
}
dfs(1,0);printf("%d\n",std::max(std::max(dp[1][0],dp[1][1]),dp[1][2]));
}
return ;
}
int main() {
int size(512<<20); // 512M
__asm__ ( "movq %0, %%rsp\n"::"r"((char*)malloc(size)+size));
Solve();
exit(0);
}
1002 Darkmoon Faire
题目难度:medium-hard
题目大意:一个数列,保证所有数互不相同。现在要把数列划分成若干段,每一段视为 1-base 的数组,要求段内最大值在奇数下标,段内最小值在偶数下标。求划分的方案数。
赛时经历:
先考虑暴力怎么做?一眼DP,令dp[i]代表以i结尾的划分方案数,那么dp[i]就枚举最后一段的长度同时求最大最小值,从dp[n-1]~dp[1]转移,答案就是dp[n],复杂度O(n^2)
考虑加速DP的过程。
首先枚举最后一段的最大最小值可以通过单调栈来维护。
然后发现最后一个区间就两种情况:起始下标、最大值下标、最小值下标为:偶偶奇/奇奇偶。
对这两个数列分别需要维护对每个起始位置x,从x开始的最大最小值的下标的奇偶性,如果最大最小值下标奇偶性合法,那么就要将其计入dp[i],考虑用线段树维护。
不妨把所有起始位置按照奇偶性分开来考虑,那么对于奇数起始位置最大最小值下标为“奇偶”,对于偶数起始位置最大最小值下标为“偶奇”。
线段树要如何维护最大最小值下标的奇偶性呢?考虑一个数一个数的加入序列,对于新加入的数,以它作为最大/最小值的起始位置x一定是一段区间,对这两个区间分别进行操作即可。
接下来分情况考虑:
令sum[root][0/1][0/1][0/1]表示当前线段树区间root,起始下标为偶/奇,最小值下标任意/合法,最大值下标任意/合法 时的区间内所有dp值之和。
起始下标为偶数,新加入的数作为最小值:这段区间的最小值下标奇偶性变成了当前下标i的奇偶性,如果i为奇数那么该段区间每一个点是否计入就只和最大值下标有关,即最小值下标合法从最小值下标任意转移;i为偶数那么该段区间每个点都不计入,即最小值合法的sum置为0。
起始下标为偶数,新加入的数作为最大值:同上
起始下标为奇数,新加入的数作为最小值:同上
起始下标为奇数,新加入的数作为最大值:同上
另外需要注意每次求解dp[i]之前要把dp[i-1]加入sum[root][0/1][0][0]
好不容易花了两个半小时写完了,16:30一交TLE,然后花了十分钟把Push_down和Push_up的传参新加入一个odd,可以让常数除二,结果16:40一交变成了WA。。。
仔细检查+对拍发现是初始化有问题,两个单调栈的top应该分别是top[0]和top[1],我写成了top[1]和top[2],16:56再交又变成了TLE。。。
此时我就慌了,然后我就继续卡常,比如把push_down和push_up内的循环展开,比如把%mod改成+-mod,比如加了快读,比如传参改成const&,一交还是TLE。。。
那我一时是真想不到什么辙了。。。就一直交一直交,希望能卡进4s。。。结果一直不行。
直到赛后半小时不断调试我才发现,关键优化是在push_down和push_up前加inline,这个优化再结合16:40的那个让push_down和push_up新加传参odd,两个优化就过了。。。
我特么裂开,什么老年机啊(恼)!!!
这次这题没做出来有两个主要原因:
①太急了,没想到inline优化,没想到杭电评测机没有默认inline优化
②写太慢了,应该把思路理清楚再开始写,尤其是线段树的状态怎么设置,同时每写完一个子函数都要认真检查保证这个子函数没错。
这次吸取教训了,下次再战!
参考代码:
include<iostream>
#include<cstdio>
#define For(i,a,b) for(int i=a;i<=b;i++)
const long long mod=998244353;
const int N=3e5+1000;
int stack[2][N],top[2];//上升,下降
int a[N];
long long dp[N];
long long sum[4*N][2][2][2];//开始位置为偶/奇,min假设/实际 max假设/实际、
long long tag[4*N][2][2];//开始位置为 偶/奇,min/max的奇偶性改变为tag
inline void Push_up(int root,int odd)
{
For(j,0,1) For(k,0,1)
{
sum[root][odd][j][k]=(sum[root<<1][odd][j][k]+sum[root<<1|1][odd][j][k]);
if (sum[root][odd][j][k]>mod) sum[root][odd][j][k]-=mod;
}
}
inline void pushdown(int root,int l,int r,int odd)
{
For(opt,0,1)
{
if (tag[root][odd][opt]==-1) continue;
int v=tag[root][odd][opt];
// printf("?? %d %d %d %d %d %d\n",root,l,r,odd,opt,v);
if (opt==0) //min
{
if (v!=odd)
{
sum[root][odd][1][1]=sum[root][odd][0][1];
sum[root][odd][1][0]=sum[root][odd][0][0];
}
else
{
sum[root][odd][1][1]=0;
sum[root][odd][1][0]=0;
}
}
else//max
{
if (v==odd)
{
sum[root][odd][1][1]=sum[root][odd][1][0];
sum[root][odd][0][1]=sum[root][odd][0][0];
}
else
{
sum[root][odd][1][1]=0;
sum[root][odd][0][1]=0;
}
}
if (l!=r) tag[root<<1][odd][opt]=tag[root<<1|1][odd][opt]=tag[root][odd][opt];
tag[root][odd][opt]=-1;
}
}
inline void Push_down(int root,int l,int r,int odd)
{
if (l==r) return ;
int mid=(l+r)>>1;
pushdown(root<<1,l,mid,odd);
pushdown(root<<1|1,mid+1,r,odd);
}
void Build(int root,int l,int r)
{
if (l==r)
{
tag[root][0][0]=tag[root][0][1]=tag[root][1][0]=tag[root][1][1]=-1;
For(i,0,1) For(j,0,1) For(k,0,1) sum[root][i][j][k]=0;
return ;
}
int mid=(l+r)>>1;
Build(root<<1,l,mid);Build(root<<1|1,mid+1,r);
tag[root][0][0]=tag[root][0][1]=tag[root][1][0]=tag[root][1][1]=-1;
For(i,0,1) For(j,0,1) For(k,0,1) sum[root][i][j][k]=0;
}
void update(int root,int l,int r,int a,int b,int odd,int opt,int v)
{
Push_down(root,l,r,odd);
if (l==a&&r==b)
{
tag[root][odd][opt]=v;
pushdown(root,l,r,odd);
return ;
}
int mid=(l+r)>>1;
if (b<=mid) update(root<<1,l,mid,a,b,odd,opt,v);
else
{
if (a>mid) update(root<<1|1,mid+1,r,a,b,odd,opt,v);
else update(root<<1,l,mid,a,mid,odd,opt,v),update(root<<1|1,mid+1,r,mid+1,b,odd,opt,v);
}
Push_up(root,odd);
}
void insert(int root,int l,int r,int x,int odd)
{
Push_down(root,l,r,odd);
if (l==r)
{
(sum[root][odd][0][0]+=dp[2*x+odd-1])%=mod;
return ;
}
int mid=(l+r)>>1;
if (x<=mid) insert(root<<1,l,mid,x,odd);
else insert(root<<1|1,mid+1,r,x,odd);
sum[root][odd][0][0]=(sum[root<<1][odd][0][0]+sum[root<<1|1][odd][0][0])%mod;
return ;
}
void read(int &x)
{
x=0;int flag=1;
char c=getchar();
while (c<'0'||c>'9')
{
if (c=='-') flag=-1;
c=getchar();
}
while ('0'<=c&&c<='9')
{
x=x*10+c-'0';
c=getchar();
}
x*=flag;
}
int main()
{
int T;read(T);
while (T--)
{
int n;read(n);For(i,1,n+1) dp[i]=0;dp[0]=1;
Build(1,0,n>>1);
top[1]=top[0]=0;
For(i,1,n)
{
read(a[i]);
// a[i]=i&1?i:-i;
insert(1,0,n>>1,i>>1,i&1);
while (top[0]>0&&a[stack[0][top[0]]]>=a[i]) top[0]--;stack[0][++top[0]]=i;
while (top[1]>0&&a[stack[1][top[1]]]<=a[i]) top[1]--;stack[1][++top[1]]=i;
int L,R;
L=stack[0][top[0]-1]+1;R=i;
if (!(L&1)) L++;if (!(R&1)) R--;
if (L<=R) update(1,0,n>>1,L>>1,R>>1,1,0,i&1);//min=a[i]: L= stack[0][top[0]-1]+1 ~ i 开始位置为奇数
// printf("?? %d %d\n",L,R);
L=stack[1][top[1]-1]+1;R=i;
if (!(L&1)) L++;if (!(R&1)) R--;
if (L<=R) update(1,0,n>>1,L>>1,R>>1,1,1,i&1);//max=a[i]: L= stack[1][top[1]-1]+1 ~ i 开始位置为奇数
// printf("?? %d %d\n",L,R);
L=stack[0][top[0]-1]+1;R=i;
if (L&1) L++;if (R&1) R--;
if (L<=R) update(1,0,n>>1,L>>1,R>>1,0,0,i&1);//min=a[i]: L= stack[0][top[0]-1]+1 ~ i 开始位置为偶数
// printf("?? %d %d\n",L,R);
L=stack[1][top[1]-1]+1;R=i;
if (L&1) L++;if (R&1) R--;
if (L<=R) update(1,0,n>>1,L>>1,R>>1,0,1,i&1);//max=a[i]: L= stack[1][top[1]-1]+1 ~ i 开始位置为偶数
// printf("?? %d %d\n",L,R);
dp[i]=(sum[1][0][1][1]+sum[1][1][1][1])%mod;
// printf("!! %d %lld\n",i,dp[i]);
}
printf("%lld\n",dp[n]);
}
return 0;
}
1007 Darnassus
题目难度:medium
题目大意:给出一个排列p ,把每个位置视为点,建一个无向图,i,j 之间的边权为|i-j|*|pi-pj|。求这个图的最小生成树。
数据范围:1≤n≤5e5
赛时情况:这题是宇彬做的,一开始随机数据能过,但一交就TLE,怀疑被卡,加了玄学优化就过了。
解题分析:
随便构造一个方案,比如1-2-3-...-n的这样一条链,它的每一条边都=|1|*|pi-pj|=|pi-pj|≤n-1
换句话说,边权>n-1的边全都可以扔了
也就是说只留下边权≤n-1的,边权≤n-1说明|i-j|≤sqrt(n-1)或|pi-pj|≤sqrt(n-1) (如果都>则边权>n-1)
那么就枚举|i-j|≤sqrt(n-1)或|pi-pj|≤sqrt(n-1) 即可,总共有2nsqrt(n)条边
然后再找到所有边以后如果直接sort会超时。。。必须得用桶排
然后如果用普通的桶排需要两倍空间,用传统的int u,v,w会MLE,需要把u和v压到一个unsigned int内。(50000*50000会爆int)(std是采用数组模拟链表来做桶排的)
即使这样还是会超时,需要在kruscal的时候一旦找到了所有n-1条边就直接break
三重优化都加上,2.375s/3s,可算过了。。。
参考代码:
查看代码
#include<iostream>
#include<cstdio>
#include<cmath>
#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--)
#define ABS(x) ((x)<0?-(x):(x))
const int N=5e4+10;
const int M=2*N*300;
struct Edge
{
unsigned int uv,w;//long long w;
} e[M],temp[M];int e_cnt,cnt[N];
int pa[N],son[N];
int find(int x){return pa[x]==-1?x:pa[x]=find(pa[x]);}
int p[N],pos[N];
int main()
{
int T;scanf("%d",&T);
while(T--)
{
int n,sqrtn;
scanf("%d",&n);sqrtn=sqrt(n-1);
For(i,1,n)
{
scanf("%d",&p[i]);
pos[p[i]]=i;
}
e_cnt=0;For(i,1,n-1) cnt[i]=0;
For(i,1,n) //|i-j|*|pi-pj|<=n-1 |i-j|<=sqrt(n-1) or |pi-pj|<=sqrt(n-1)
{
For(j,i+1,n)
{
if (j-i>sqrtn) break;
unsigned int w;
w=(unsigned int)(j-i)*ABS(p[j]-p[i]);
if (w<=n-1) temp[++e_cnt]=(Edge){(unsigned int)i*n+j-1,w},cnt[w]++;
w=(unsigned int)(j-i)*ABS(pos[j]-pos[i]);
if (w<=n-1) temp[++e_cnt]=(Edge){(unsigned int)pos[i]*n+pos[j]-1,w},cnt[w]++;
}
}
For(i,1,n-1) cnt[i]+=cnt[i-1];
For(i,1,e_cnt) e[cnt[temp[i].w]--]=temp[i];
long long ans=0,now=0;
For(i,1,n) pa[i]=-1,son[i]=1;
For(i,1,e_cnt)
{
int u=e[i].uv/n,v=e[i].uv%n+1;
if (find(u)==find(v)) continue;
if (son[find(u)]<son[find(v)])
pa[find(u)]=find(v),son[find(v)]+=son[find(u)];
else
pa[find(v)]=find(u),son[find(u)]+=son[find(v)];
ans+=e[i].w;
now++;if (now==n-1) break;
}
printf("%lld\n",ans);
}
return 0;
}