牛客网NOIP赛前集训营-提高组(第七场)Solution
先吐槽一下,这次的比赛稍微有点水了。。。\(kcz\) 神仙 \(40min\) 做完 \(3\) 道题 \(rank1\) 到最后竟然掉了 \(73\) 分。。。
Problem A. 中国式家长 2
大水题,直接按题意模拟即可,没什么好说的,全场切,\(kcz\) 神仙 \(6min\) 切
#include<cstdio>
#include<iostream>
using namespace std;
const int N=2e2+10;
int n,m,k,T,a[N][N],x,y,Walk,IQ;
int dx[8]={-1,-1,0,1,1,1,0,-1},dy[8]={0,1,1,1,0,-1,-1,-1};
bool b[N][N],Dig[N][N];
int main(){
scanf("%d%d%d",&n,&m,&k);Walk=k;
for(register int i=1;i<=n;i++)
for(register int j=1;j<=m;j++)
scanf("%d",&a[i][j]);
for(register int i=1;i<=n;i++)
for(register int j=1;j<=m;j++)
scanf("%d",&b[i][j]);
scanf("%d",&T);
while(T--){
scanf("%d%d",&x,&y);
if(!b[x][y]||Dig[x][y]||(!a[x][y]&&Walk<10)){printf("-1 -1\n");return 0;}
Dig[x][y]=true;
for(register int i=0;i<=7;i++)b[x+dx[i]][y+dy[i]]=true;
if(!a[x][y])Walk-=10,IQ+=10;
else Walk=min(Walk+a[x][y],k);
}
printf("%d %d\n",Walk,IQ);return 0;
}
Problem B. 随机生成树
问题可以转化为把 \(n\) 个点按题意连接后,颜色相同且联通的一块被称为一个联通块,求最多有多少联通块
题目要求它的父亲必须是它的约数,那么我们考虑枚举每一个数的约数,显然这样做复杂度是 \(O(n\sqrt n)\) 级别的,这也是为什么本题时限竟然有 \(2s\),因为常数小一点这样的乱搞也能过。(PS:我最近都被暴力吓怕了,前两次模拟赛有三道题全部给了暴力满分,这回暴力写的好也过了,真不知道出题人是怎么想的)
我们当然不会满足于这样的乱搞算法,所以很快,我们就想到另一种更优的算法:直接对于每个数枚举它的倍数,然后对应更新答案即可。因为要让联通块尽量的多,那么每个点都要尽量往与它颜色不同的节点上连,这样贪心下去就可以保证是最优的了,复杂度大概是 \(O(n\log n)\),所以这道题数据范围扩大到 \(5000000\) 都没有问题。
这题 \(kcz\) 只用了 \(4min\) 就切掉了。。。
#include<cstdio>
#include<iostream>
using namespace std;
const int N=5e5+10;
int n,a[N],fa[N],head[N],cnt,pre[N],ans;
struct edge{int nxt,to;}ed[N];
inline void addedge(int x,int y){ed[++cnt].to=y;ed[cnt].nxt=head[x];head[x]=cnt;}
inline void DFS(int u){
pre[u]=ans;
for(register int i=head[u];i;i=ed[i].nxt)
if(a[u]==a[ed[i].to])DFS(ed[i].to);
}
int main(){
scanf("%d",&n);
for(register int i=1;i<=n;i++)scanf("%d",&a[i]);
for(register int i=1;i<=n;i++){
if(i!=1)addedge(fa[i],i);
for(register int j=(i<<1);j<=n;j+=i)
if(!fa[j])fa[j]=i;else if(a[i]!=a[j])fa[j]=i;
}
for(register int i=1;i<=n;i++)if(!pre[i])++ans,DFS(i);
printf("%d\n",ans);return 0;
}
Problem C. 洞穴
这题思想挺不错的,当时在考场上已经想差不多了,就是设 \(dis[i][j][k]\) 表示从 \(i\) 走 \(k\) 步是否可以到达 \(j\),但没想到倍增,硬设肯定是不行的,毕竟最多要走十亿多步,然后就陷入了图中环的深渊无法自拔,瞎转移一波成功得到了 \(5\) 分。。。接下来说说正解
还是和上面差不多,我们设 \(dis[i][j][k]\) 表示从 \(i\) 走 \(2^j\) 步是否可以到达 \(k\),然后我们发现这东西的值只有 \(0\) 和 \(1\),所以直接用 \(bitset\) 把最后一维压掉,这样位运算的复杂度就变成了 \(O(N/64)\) 了,可以加快速度
然后考虑转移,如果 \(i\) 走 \(2^j\) 步可以到达 \(k\),那么显然 \(i\) 走 \(2^{j+1}\) 步就可以达到 \(k\) 走 \(2^j\) 步所能达到的所有节点,那么就有以下的转移方式
\[dis[i][j+1]|=dis[k][j],dis[i][j][k]==1
\]
这样我们就完成了预处理
那么我们这样倍增到底有什么好处呢?可以发现,对于每一个询问里的 \(l\),它在二进制下都由若干个 \(1\) 组成,那么我们只要找到这些 \(1\),并将当前可到达的节点倍增走出去即可。例如当前可以走到的节点状态为 \(ans\) (\(bitset\) 压位),那么我们就可以枚举 \(ans\) 所有不为 \(0\) 的位置,然后用 \(dis[i][j]\) 再往外走就行了
复杂度大概为 \(O(\frac{Qlogln^2}{64}+\frac{n^3logn}{64})\),代码如下:
#include<cstdio>
#include<iostream>
#include<bitset>
using namespace std;
const int N=1e2+10;
const int LOG=31;
int n,m,U,V,l,a,b,Q;
bitset<N>dis[N][33],ans,tmp;
int main(){
scanf("%d%d",&n,&m);
for(register int i=1;i<=m;i++)
scanf("%d%d",&U,&V),dis[U][0][V]=1;
for(register int j=0;j<=LOG;j++)
for(register int i=1;i<=n;i++)
for(register int k=1;k<=n;k++)
if(dis[i][j][k])dis[i][j+1]|=dis[k][j];//若数组紧贴着LOG开这里加一会超过范围!!!
scanf("%d",&Q);
while(Q--){
scanf("%d%d%d",&l,&a,&b);
ans.reset();ans[a]=1;
for(register int i=0;i<=LOG;i++){
if(!(l>>i))break;
if((l>>i)&1){
tmp=ans;ans.reset();
for(register int j=1;j<=n;j++)if(tmp[j])ans|=dis[j][i];
}
}
puts(ans[b]? "YES":"NO");
}
return 0;
}