牛客CSP-S提高组赛前集训营1

比赛链接

官方题解

before:T1观察+结论题,T2树形Dp,可以换根或up&down,T3正解妙,转化为图上问题。题目质量不错,但数据太水了~。

A-仓鼠的石子游戏

题目描述

一共n个石子堆,每个石子堆有ai个石子,两人轮流对石子涂色(先手涂红,后手涂蓝),且需要保证当前回合涂的石子颜色不能和它相邻的两个同色,谁涂不下去谁输。一共T个询问,对于每个询问输出先手必胜还是后手必胜。

\(1<=n<=10^3,1<=ai<=10^9,1<=T<=10^2\)

题解

标签:模拟,猜结论

当n=1时,模拟一下可以发现,除非石子数=1,否则后手必胜。当增加多堆石子时,假设当前局面是P必败,则P肯定要去新的一堆里涂,则对于新的一堆石子P成为了先手,假设新的一堆石子数>1,则P任然必败,反之可以转移,P的另一方成为了下一堆的先手。也就是说只有数量为1的石子堆能够改变胜负状态。

所以,只需统计石子数为1的堆的数量\(cnt1\),根据\(cnt1\)的奇偶性来判断。若为奇,则先手必胜;反之先手必败。

#include<bits/stdc++.h>
using namespace std;
inline int read(){}
int main(){
int T=read();
while(T--){
int n=read();
bool o=0;
for(register int i=1;i<=n;i++){
int x=read();
if(x==1)o^=1;
}
if(o==0)puts("hamster");
else puts("rabbit");
}
}

B- 乃爱与城市拥挤程度

题目描述

见原题面

题解

标签:树形Dp,换根法,Up and Down

暴力做法是\(O(N^2)\)的,将每个点都提做根跑一遍树形dp。优化很明显是去换根

由于标程给的换根法写的非常板子(虽然有点长),所以这里按标程的思路走一遍,整理一下换根法的大致思路。

step1:预处理dfs

随便找一个节点作为根,进行dfs/树形Dp,处理出所有节点此时的答案(只考虑自己子树下的答案)。

step2:换根

平时做题打换根都是直接去统计,加加减减,虽然码量小但不同的题目细节不同,调试起来不太方便。标程中将换根分为两部分。第一部分\(cut\),先分别消除彼此的影响,逆操作;第二部分\(link\),对更改后的数组正操作一遍。

在本题中有两个询问。弄两个数组分别针对题目的两个询问,第一个比较好理解,第二个比较抽象,根据代码y一下。

\(dp[x][i]\):换根前,只考虑x的子树时,与x距离为i的子孙数量。

\(dp2[x][i]\):换根前,只考虑x的子树时,当影响范围为i时,x影响到的子孙的拥挤程度之乘积。

将节点1作为根,预处理dfs一遍。下面的操作都是将\(sonx\)的贡献加给\(x\)的正操作

void dp_dfs(int x,int fa){
dp[x][0]=1;
for(int i=0;i<=k;++i)dp2[x][i]=1;
for(int i=0;i<G[x].size();++i){
if(G[x][i]!=fa){
dp_dfs(G[x][i],x);
for(int j=1;j<=k;++j){
dp[x][j]+=dp[G[x][i]][j-1];
dp2[x][j]=dp2[x][j]*dp2[G[x][i]][j-1]%mod;
}
}
}
long long sum=0;
for(int i=0;i<=k;++i){
sum+=dp[x][i];
dp2[x][i]=dp2[x][i]*sum%mod;
}
}

这一遍dfs后,此时的根节点1已经可以直接统计出答案了,但对于其他非根节点来说,它们只考虑了自己子树内的贡献,还需考虑子树外的贡献。

接下来再dfs一遍,换根。

void change_root(int root1,int root2) {
//原来的根:root1 现在要换为:root2,且root1之前是root2的父节点
cut_dp(root1,root2);//逆操作
link_dp(root2,root1);//正操作,重新统计一遍
}

逆操作、正操作都与之前的预处理dfs类似。

标程的换根法代码

无根树Dp还可以up and down去做。网上关于这一块内容不多,而且很多blog都说和换根法差不多。

比如对于这题的询问一,要求树中离x距离小于等于k的节点数目,可以分解为这样两个问题,①x子树中距离小于等于k的节点数目+②x子树外距离小于等于k的节点数目,对这两个问题分别dfs一遍,问题①用儿子更新自己,问题②用自己更新儿子,最后将两部分的贡献合起来。好像确实和换根差不多,但实现起来感觉up&down的思路更清晰些。

下面是up&down的代码

#include<bits/stdc++.h>
#define Mod 1000000007
#define int long long
using namespace std;
const int N=1e5+10;
vector<int>g[N];
int num[N][11],dp[N][11];
int outnum[N][11],outdp[N][11];
int n,k,ans1[N],ans2[N];
namespace UpandDown{
int ksm(int x,int d){
int res=1;
while(d){
if(d&1)res=res*x%Mod;
x=x*x%Mod;d>>=1;
}
return res;
}
void dfs(int x,int fa){
for(int i=0;i<g[x].size();i++)if(g[x][i]!=fa)dfs(g[x][i],x);
num[x][0]=1,dp[x][0]=1;
for(int i=1;i<=k;++i){
num[x][i]=1,dp[x][i]=1;
for(int j=0;j<g[x].size();++j){
int y=g[x][j];if(y==fa)continue;
num[x][i]+=num[y][i-1];
dp[x][i]=dp[x][i]*dp[y][i-1]%Mod;
}
dp[x][i]=dp[x][i]*num[x][i]%Mod;
}
}
void redfs(int x,int fa){
if(x==1){
ans1[x]=num[x][k];
ans2[x]=dp[x][k];
}
else{
ans1[x]=num[x][k]+outnum[x][k];
ans2[x]=dp[x][k]*ksm(num[x][k],Mod-2)%Mod*(num[x][k]+outnum[x][k])%Mod*outdp[x][k]%Mod;
}
for(int i=0;i<g[x].size();i++){
int y=g[x][i];if(y==fa)continue;
outnum[y][1]=1,outdp[y][1]=1;
for(int j=2;j<=k;j++){
outnum[y][j]=outnum[x][j-1]+num[x][j-1]-num[y][j-2];
outdp[y][j]=outdp[x][j-1]*dp[x][j-1]%Mod*ksm(dp[y][j-2],Mod-2)%Mod*
ksm(num[x][j-1],Mod-2)%Mod*(num[x][j-1]-num[y][j-2]+outnum[x][j-1])%Mod;
}
redfs(y,x);
}
}
void solve(){
for(int i=1;i<=k;i++)for(int j=1;j<=k;j++)outdp[i][j]=1;
dfs(1,0),redfs(1,0);
for(int i=1;i<=n;i++)printf("%lld ",ans1[i]);
printf("\n");
for(int i=1;i<=n;i++)printf("%lld ",ans2[i]);
}
}
signed main(){
scanf("%lld%lld",&n,&k);
for(int i=1;i<n;i++){
int u,v;scanf("%lld%lld",&u,&v);
g[u].push_back(v);
g[v].push_back(u);
} UpandDown::solve();
return 0;
}

C- 小w的魔术扑克

题目描述

小w有\(k\)张魔术扑克,第i张魔术扑克有两面数值\(ai,bi\)(可能相等),牌面的数值在\([1,n]\)。现在有q个询问,每个询问有两个整数\(l,r\),询问是否能用若干张魔术扑克凑成\(l,l+1,..,r-1,r\)这个顺子,每张魔术扑克只能使用一面的值。

对于30%测试点,\(1<=n<=11\),\(1<=k<=10\),\(1<=q<=100\)。

对于60%测试点,\(1<=n,k<=50\),\(1<=q<=500\)。

对于100%测试点,\(1<=n,k,q<=10^5\),\(1<=l<=r<=n\)。

题解

标签:思维题,图论,并查集,离线询问,树状数组

30pts

二进制枚举。\(O(2^n*n)\)。

60pts

这种一一匹配的关系容易联想到二分图。令左边为\([1,n]\)的牌面,右边为魔术扑克的编号。建立两两联通关系,对于每个牌面l(\(l∈[1,n]\)),维护一个\(Ma[l]\),表示若能打出l,则能打出的连续最右位置为Ma[l],那么对于每个询问\((l,r)\),只用判断\(Ma[l]>=r?可行:不可行\)。

如何维护?上面已经提到二分图了,如果直接枚举l,Ma[l],然后再用匈牙利算法判一下,复杂度比较大。由于这个Ma显然具有单调性,考虑一边尺取,一边跑匈牙利算法

代码如下,时间复杂度为\(O(N^3)\),但是由于垃圾数据赛时是可以A了这题的。

#include<bits/stdc++.h>
using namespace std;
const int N=1e5+5;
int n,m,bf[N],gf[N];
int mark[N],Ma[N];
vector<int>g[N];
bool grab(int x){
if(mark[x])return 0;
mark[x]=1;
for(int i=0;i<g[x].size();i++){
int y=g[x][i];
if(!bf[y]||grab(bf[y])){
bf[y]=x;
gf[x]=y;
return 1;
}
}
return 0;
}
int main(){
scanf("%d%d",&n,&m);
for(int i=1;i<=m;i++){
int x,y;scanf("%d%d",&x,&y);
g[x].push_back(i);
g[y].push_back(i);
}
for(register int l=1,r=0;l<=n;l++){
bf[gf[l-1]]=0;
r=max(r,l-1);
memset(mark,0,sizeof(mark));
while(r<n&&grab(r+1)){
r++;
memset(mark,0,sizeof(mark));
}
Ma[l]=r;
}
int q,l,r;scanf("%d",&q);
for(register int i=1;i<=q;i++){
scanf("%d%d",&l,&r);
if(Ma[l]>=r)puts("Yes");
else puts("No");
}
}

100pts

数据范围加到\(10^5\)。把问题拎到图上去,建模。

对于一张魔术扑克\((a,b)\),将编号为\((a,b)\)的节点先连一条无向边,这样会连出共\(k\)条边,可能会有重边,可能会形成多个联通块,每个联通块可能包含环也可能只是一棵树。现在对于询问\((l,r)\),你需要将其中的若干条无向边变成有向边,也就是指定这条边指向两个端点中的一个,使得\((l,...,r)\)中的点的入度均不为0。

现在这个问题就是原问题套件衣服,不过现在到了图上看起来比较可做?。

引理1:对于无向连通图S。试将所有无向边改为有向边,当且仅当S中存在至少一个环(包括自环)时,能够使得S中的所有节点入度不为0。

证明:如果不存在环,那就是一棵普通的树,对于每条边,最优排布时应该是让每条边都指向深度较大的那个(指向儿子),但最后对于根节点,它的入度为0,不合法。如果这时候树上出现了环,由人类智慧可知,会有多余的边剩给根节点,此时一定合法。

也就是说,当我们询问的\((l,r)\)时,只要存在一个不含环的联通块,满足这个联通块内的节点编号均被l,r覆盖,则不存在可行解能打出\([l,r]\)的顺子。

这就变成了一个简单的线段覆盖问题。接下来做法就很多了树状数组,set之类的,可以参考官方题解。下面给出一种较简洁的做法。

理一遍流程,在输出魔术扑克\((a,b)\)时,利用并查集维护联通块的信息,如果此前\(a,b\)已经在同一联通块,则合并这条边后,这个联通块就有了环,标记一下。连完边后,找出所有不含环的联通块,记该联通块内编号最大的节点为\(ma\),编号最小的节点为\(mi\),则视\([mi,..,ma]\)为一条线段,对于询问直接看\((l,r)\)会不会把某条线段覆盖了,如果会覆盖某条线段则输出No,反之Yes。看会不会把某条线段覆盖可以离线询问,然后对询问和原有线段都排个序扫一遍就好了。

时间复杂度为\(O(nlogn)\)。

代码如下:

#include<bits/stdc++.h>
using namespace std;
const int N=1e5+10;
int n,m;
int fa[N],mi[N],ma[N],huan[N],cover[N];
int find(int x){return fa[x]==x?x:fa[x]=find(fa[x]);}
struct seg{
int l,r,id;
}que[N];
vector<seg>g;
inline bool cmp(seg p,seg q){return p.r<q.r;} int main(){
scanf("%d%d",&n,&m);
for(int i=1;i<=n;i++)fa[i]=mi[i]=ma[i]=i;
for(int i=1;i<=m;i++){
int x,y;scanf("%d%d",&x,&y);
int A=find(x),B=find(y);
if(A==B)huan[A]=1;
else{
fa[A]=B;
huan[B]|=huan[A];
ma[B]=max(ma[B],ma[A]);
mi[B]=min(mi[B],mi[A]);
}
}
for(int i=1;i<=n;i++){
if(find(i)==i&&!huan[i])g.push_back((seg){mi[i],ma[i],0});
}
int q;
scanf("%d",&q);
for(int i=1,a,b;i<=q;i++){
scanf("%d%d",&a,&b);
que[i]=(seg){a,b,i};
}
sort(g.begin(),g.end(),cmp);
sort(que+1,que+q+1,cmp);
for(int i=1,cur=0,maxl=0;i<=q;i++){
while(cur<g.size()&&g[cur].r<=que[i].r){
maxl=max(maxl,g[cur].l);cur++;
}
if(maxl>=que[i].l)cover[que[i].id]=1;
}
for(int i=1;i<=q;i++)puts(cover[i]?"No":"Yes");
}

牛客CSP-S提高组赛前集训营1的更多相关文章

  1. 牛客网CSP-S提高组赛前集训营Round4

    牛客网CSP-S提高组赛前集训营 标签(空格分隔): 题解 算法 模拟赛 题目 描述 做法 \(BSOJ6377\) 求由\(n\)长度的数组复制\(k\)次的数组里每个连续子序列出现数字种类的和 对 ...

  2. 牛客CSP-S提高组赛前集训营3

    A 货物收集 显然是一个二分答案的题. #include<iostream> #include<cstdio> #include<cstring> #include ...

  3. 牛客CSP-S提高组赛前集训营3 赛后总结

    货物收集 二分答案.复杂度\(O(n\log n)\). 货物分组 用费用提前计算的思想,考虑用一个新的箱子来装货物会发生什么. 显然费用会加上后面的所有货物的总重. \(60\)分的\(O(n^2) ...

  4. 牛客CSP-S提高组赛前集训营2 ———— 2019.10.31

    比赛链接 期望得分:100+20+20 实际得分:40+20+30 awa  cccc T1 :基于贪心的思路,然后开始爆搜(雾 那必然是会死的,好吧他就是死了 #include<iostrea ...

  5. 牛客CSP-S提高组赛前集训营1———2019.10.29 18:30 至 22:00

    期望得分:100+0+10 实际得分:40+0+0 考炸了... T1:题目链接 究竟为什么会这样,,, 仔细研读我的丑代码 发现... 枯辽.... #include<cstdio> # ...

  6. 牛客CSP-S提高组赛前集训营2 T2沙漠点列

    原题链接 算法不难,比赛的时候就和cyc大佬一起yy了正解,不过因为交的时候比较急(要回寝室惹),我有两数组开错大小直接爆到50,cyc大佬则只把文件输入关了一半,直接爆零(╯ ̄Д ̄)╯┻━┻ 要尽量 ...

  7. 20191029 牛客CSP-S提高组赛前集训营1

    前一个小时看这几道题感觉要爆零 A. 仓鼠的石子游戏 分析一下发现a[i]>1a[i]>1a[i]>1时后先手必输,a[i]=1a[i]=1a[i]=1时先手必赢 然后直接看1的个数 ...

  8. 牛客CSP-S提高组赛前集训营4 赛后总结

    复读数组 分成 3 种区间算答案: 一个块内的区间 两个块交界处,长度小于块长的区间 长度不小于块长的区间 对于第三种区间,容易发现每个区间的权值一样,只需要算出个数即可. 对于前两种空间,我的思路是 ...

  9. 牛客CSP-S提高组赛前集训营5 赛后总结

    A.无形的博弈 心理题. 答案为\(2^n\),可感性理解结论的正确性. #include<bits/stdc++.h> #define LL long long const LL Mod ...

随机推荐

  1. 20140604 word表格中打钩 循环右移

    1.如在在word表格中打钩 符号->其他符号->字体(wingdings2) 2.循环右移 方法1: #include<stdio.h> void move(char *s) ...

  2. python:Django 简介。

    Django是基Python的重要WEB框架. 1.安装Django Web框架 安装  pip 安装 在命令行模式 执行  [pip install django == 2.0]或者 [pip in ...

  3. Sublime Text3中MarkDown的使用

    前言 当我们想要在Sublime文本编辑器中编辑markdown时,需要先安装markdown插件,因为Sublime里默认没有安装该插件,同时在编辑markdown文本时可以实时预览编辑效果. 具体 ...

  4. vue实现curd功能

    一.实现效果 二.实现 (一)实现增加用户功能 Vuserlist组件中 <template> <div class="panel panel-default"& ...

  5. wordpress非弹窗表单多文件上传插件

    插件名:Drag and Drop Multiple File Upload - Contact Form 7 (ps:如果是弹窗表单,不要使用这个插件Drag and Drop Multiple F ...

  6. Android Studio Download

    { https://developer.android.google.cn/studio }

  7. hdu第九场多校

    02:线段树两次扫描 #include<bits/stdc++.h> #include<vector> using namespace std; #define maxn 30 ...

  8. NX二次开发-C++ DeleteFile删除文件实例代码

    NX9+VS2012 #include<Windows.h> DeleteFile("D:\\1\\test123.prt"); Caesar卢尚宇 2019年7月29 ...

  9. 牛客多校第十场 E Hilbert Sort 递归,排序

    题意: 给你一个方阵,再在方阵上给定一些点,按照希尔伯特曲线经过的先后顺序为这些点排序 题解: 定义好比较函数后直接调用排序算法即可. 希尔伯特曲线本来就是用于二维到一维的映射的,因此我们可以考虑对于 ...

  10. map和unordered_map使用小结

    map和unordered_map unordered_map简介: #include <cstdio> #include <iostream> #include <un ...