牛客小白赛 65 题解
牛客小白赛 65
A. 牛牛去购物
标签
统计类 DP
思路
- 因为值域很少,故可设 \(dp_i\) 表示 \(i\) 是否出现过。则初始化为 \(dp_{0}=1\),之后分别从 \(1\) 到 \(x\) 进行转移方程 \(dp_i|=dp_{i-x},i-x\ge 0\),其中 \(x=a,b\)。
- 时间复杂度为 \(\mathcal O(T)\),\(T\) 为值域。
代码
点击查看代码
#include<bits/stdc++.h>
using namespace std;
const int maxn=1005;
int dp[maxn],n,a,b;
int main()
{
cin>>n>>a>>b;
dp[0]=1;
for(int i=a;i<=n;i++)
dp[i]|=dp[i-a];
for(int i=b;i<=n;i++)
dp[i]|=dp[i-b];
int ans=0;
for(int i=1;i<=n;i++)
if(dp[i])ans=max(ans,i);
printf("%d",n-ans);
return 0;
}
B. 牛牛写情书
标签
KMP
字符串
思路
- 将特殊字符的 ascii 码统计下来,遍历字符串 \(a\),将非特殊字符加入到新字符串 \(a'\) 中。
- 以 \(b\) 为模式串,\(a'\) 为主串进行 KMP。
- 时间复杂度为 \(\mathcal O(\max(len_a,len_b))\)
代码
点击查看代码
#include<bits/stdc++.h>
using namespace std;
const int maxn=5e3+5;
const char consts[39]={'0', '1', '2', '3',
'4', '5', '6', '7',
'8', '9', '+', '-',
'*', '|', ',', '.',
'~', '!', '@', '#',
'$', '%', '^', '&',
'(', ')', '[', ']',
'{', '}', '"', ';',
':', '?', '<', '>',
'\\', '/'};
char a[maxn],b[maxn],sa[maxn];
int n,m,nxt[maxn],vis[300],ni;
int main()
{
for(int i=0;i<39;i++)
vis[int(consts[i])]=1;
vis[39]=1;
scanf("%d%d",&n,&m);
cin>>a+1>>b+1;
for(int i=1;i<=n;i++)
if(!vis[int(a[i])])
sa[++ni]=a[i];
for(int i=2,j=0;i<=m;i++)
{
while(j&&b[i]!=b[j+1])
j=nxt[j];
j+=(b[i]==b[j+1]);
nxt[i]=j;
}
for(int i=1,j=0;i<=ni;i++)
{
while(j&&sa[i]!=b[j+1])
j=nxt[j];
j+=(sa[i]==b[j+1]);
if(j==m)
{
cout<<"YES";
return 0;
}
}
printf("NO");
return 0;
}
C. 牛牛排队伍
标签
链表
模拟
思路
- 链表板子题。
- 时间复杂度为 \(\mathcal O(n)\)。
代码
点击查看代码
#include<bits/stdc++.h>
using namespace std;
const int maxn=1e6+10;
int n,k;
struct people
{
int next,from;
} p[maxn];
int main()
{
scanf("%d%d",&n,&k);
for(int i=1;i<=n;i++)
{
p[i].from=i-1;
p[i].next=i+1;
}
int op,x;
while(k--)
{
scanf("%d%d",&op,&x);
if(op==2) printf("%d\n",p[x].from);
else
{
p[p[x].from].next=p[x].next;
p[p[x].next].from=p[x].from;
}
}
return 0;
}
D. 牛牛取石子
标签
博弈论
思路
- 首先考虑一次性结束游戏的状态。若 \(a=1,b=1\),则 \(niumei\) 胜;若 \(2<a+b\le4\),则 \(niuniu\) 胜。
- 考虑多次游戏的情况。发现 $(1,2)+(2,1)=(3,3) $,故后手总可以使得每一轮操作后使得两边石子数均减少 \(3\)。若 \(a\ne b\),设较少的一堆为 \(x\)则:若 \(3|x\),则后手必胜,此时后手为 \(niumei\);若 $x%3=1 $,则 \(niuniu\) 令数值为 \(x\) 的堆减一,易验证另一堆减少后的数量一定不小于 \(x-1\),此时后手必胜,后手为 \(niuniu\);若 \(a\%3=2\),同理可得,\(niuniu\) 必胜。若 \(a=b\),则:若 \(3|a\),显然 \(niumei\) 胜;若 $a%3=1 $,显然 \(niumei\) 胜;若 $a%3=1 $,此时 \(niumei\) 一定知道每一轮都让两边石子按照 \(3\) 递减不合适,故其会在 \(niuniu\) 操作后采取与其相同的操作,导致较小的一堆在模 \(3\) 下等于 \(1\),此时后手必胜,后手为 \(niuniu\)。
- 时间复杂度为 \(\mathcal O(T)\)。
代码
点击查看代码
#include<bits/stdc++.h>
#define ll long long
using namespace std;
ll a,b;
int T;
int main()
{
scanf("%d",&T);
while(T--)
{
scanf("%lld%lld",&a,&b);
if(a==1&&b==1)
{
printf("niumei\n");
continue;
}
if(a==1||b==1||a==2||b==2) printf("niuniu\n");
else
{
if(a!=b)
{
ll x=min(a,b);
if(x%3==0) printf("niumei\n");
else printf("niuniu\n");
}
else
{
if(a%3==0) printf("niumei\n");
else if(a%3==1) printf("niumei\n");
else printf("niuniu\n");
}
}
}
return 0;
}
E. 牛牛的构造
标签
构造
思路
- 因为满足要求的数对与次序有关,故先考虑极端情况——递增与递减序列。考虑后易得,递增数列任意数对答案的贡献均为 \(0\),递减数列数列可以满贡献,且其满贡献的实质是所有比他小的数均在其后面。故可以考虑构造 \(a\) 的形式为递减数列+递增数列,因前面为递减数列,故可以保证递减数列中的任意元素满贡献。
- 考虑某个数 \(i\) 的以 \(i\) 为数对中大数的满贡献 \(dp_i\) 是多少。容易发现,与之贡献的数 \(x\),有 \(i-x=2^y\),进而有 \(x=i-2^y,y\ge 0\),故 \(dp_i=[\log_{2}^{i-1}]+1\)。发现 \(dp\) 数列中的数连续。
- 基于 \(dp\) 中的数连续,可构造数列 \(a\)。若 \(\sum\limits_{1}^{n}dp_i < k\),则无法构造。否则,设 \(res\) 表示目前剩余的需要构造的数对数,从 \(n\) 到 \(1\) 遍历 \(dp\),若 \(dp_i\le res\),则将 \(i\) 压入递减数列,\(res\) 减少 \(dp_i\);否则 \(i\) 压入递增数列。因为 \(dp\) 连续,故总可以使得 \(res\) 为 \(0\)。
- 时间复杂度为 \(\mathcal O(n)\)。
代码
点击查看代码
#include<bits/stdc++.h>
#define ll long long
using namespace std;
const int maxa=1e6+5;
int n,k,inc[maxa],d[maxa],p,pi;
ll dp[maxa],sum;
int main()
{
scanf("%d%d",&n,&k);
for(int i=1;i<=n;i<<=1)
dp[i+1]=1;
for(int i=1;i<=n;i++)
dp[i]=dp[i-1]+dp[i],
cout<<i<<" "<<dp[i]<<endl;
ll num=0;
for(int i=1;i<=n;i++)
num+=dp[i];
if(num<k) printf("-1");
else
{
for(int i=n;i>=1;i--)
{
if(k<dp[i]) inc[++p]=i;
else
{
d[++pi]=i,k-=dp[i];
}
}
for(int i=1;i<=pi;i++)
cout<<d[i]<<" ";
for(int i=p;i>=1;i--)
cout<<inc[i]<<" ";
}
return 0;
}
F. 牛牛的考试
标签
贪心
树上 DP
思路
- 设 \(tot_i\) 表示以 \(i\) 为根节点的整颗子树的权值和,\(ans_i\) 表示 完成整颗子树所需的最少时间。根据贪心,可知应当使双课程同时进行的值尽可能的大。故我们假设单位时间为 \(1\) 个馒头,则相当于在子节点 \(j\) 的位置累了 \(tot_j\) 个馒头,目标是获得每次取走不同位置上的两个馒头,最多可以取多少次。记 \(sum=\sum\limits_{j\in son_i} tot_j\),则易得最多有一摞馒头的个数大于 \([\frac{sum+1}{2}]\)。按馒头数由大到小,遍历每一摞馒头,并把其的馒头尽可能地放到最接近累满 \([\frac{sum+1}{2}]\) 个馒头的位置。易得,最后会获得两摞馒头,且每一层的馒头都是不同的两种(如果这层有两个馒头的话)。但是,若 \(\max\limits_{j\in son_i}\{tot_j\}>[\frac{sum+1}{2}]\),则无论如何完成这些子任务都要 \(ans_{j_0|tot_{j_0}=\max\limits_{j\in son_i}\{tot_j\}}\) 的时间;否则,只需 \([\frac{sum+1}{2}]\) 的时间完成所有的子任务。故 \(ans_i=a_i+\max([\frac{sum+1}{2}],ans_{j_0|tot_{j_0}=\max\limits_{j\in son_i}\{tot_j\}})\),\(tot_i=a_i+\sum\limits_{j\in son_i}tot_j\)。
- 时间复杂度为 \(\mathcal O(n)\)。
代码
点击查看代码
#include<bits/stdc++.h>
#define ll long long
using namespace std;
const int maxn=1e6+5;
int n,a[maxn];
ll tot[maxn],ans[maxn];
int c[maxn];
vector<int> son[maxn];
void dfs(int u)
{
ll sum=0,toti,tmxi;
if(son[u].size()==0)
{
ans[u]=tot[u]=a[u];
return;
}
for(int i=0;i<son[u].size();i++)
{
int v=son[u][i];
dfs(v);
tot[u]+=tot[v];
ans[u]=max(ans[u],ans[v]);
}
ans[u]=max(ans[u],(1+tot[u])>>1);
ans[u]+=a[u],tot[u]+=a[u];
}
int main()
{
scanf("%d",&n);
for(int i=1;i<=n;i++)
scanf("%d",&a[i]);
for(int i=1;i<n;i++)
scanf("%d",&c[i]),
son[c[i]].push_back(i+1);
dfs(1);
printf("%lld",ans[1]);
return 0;
}