CF#1044(Div.2)全解
CodeForces Round 1044(Div.2)
完成度:完结。
个人感觉:
- 思维:A<C<B<E<D<F
- 代码:ABD<C<E<F
A
自评:橙。
出题人试图伪装了,但还是很明显。结论是相同齿数的转速一定相同,然后开个桶或者排序扫一遍看看有没有相同的就行了。
sort(a,a+n);
bool flg=0;
for(int i=1;i<n;i++){
if(a[i]==a[i-1]){
flg=1;
break;
}
}
if(flg)cout<<"YES";
else cout<<"NO";
cout<<endl;
B
自评:黄。
这个比较降智,我是花了些时间想的。
即使瞪不出来,手玩样例也可以发现,连一条边,边的至少一端会变成
所以直接排个序从大到小每两个一连边就出现了一堆含
sort(a,a+n);
ll ans=0;
for(int i=n-1;i>=0;i-=2){
ans+=a[i];
}
cout<<ans<<endl;
C
自评:绿。
首先排着问一遍直接得到起点,此时一共问了
然后显然的假设长度为
于是把所有询问结果是
实现时拿 vector 按第一次的询问结果将点分类即可,单组复杂度
cin>>n;
ll mx=0;
ans.clear();
for(int i=1;i<=n;i++)a[i].clear();
for(int i=1;i<=n;i++){
cout<<"? "<<i<<' '<<n;
for(int i=1;i<=n;i++)cout<<' '<<i;
cout<<endl;
ll tmp;
cin>>tmp;
a[tmp].push_back(i);
mx=max(mx,tmp);
}
ans.push_back(a[mx--][0]);
while(mx>0){
for(int i=0;i<a[mx].size();i++){
cout<<"? "<<ans[ans.size()-1]<<' '<<n-a[mx].size()+1;
for(int j=1;j<=n;j++){
if(j!=mx){
for(int k=0;k<a[j].size();k++)cout<<' '<<a[j][k];
}
else cout<<' '<<a[j][i];
}
cout<<endl;
ll tmp;
cin>>tmp;
if(tmp==mx+1){
ans.push_back(a[mx][i]);
break;
}
}
mx--;
}
cout<<"! "<<ans.size();
for(int i=0;i<ans.size();i++){
cout<<' '<<ans[i];
}
cout<<endl;
:::info[额外的挑战(1)]
在不超过
我们注意到在找路径的过程中,有一些东西是冗余的。对于每一层,我们可以二分点的集合,每次加入该层二分范围内一半的点,而不是逐个查找,则最坏的情况下该层有
:::info[额外的挑战(2)]
在期望
随机化即可。我们打乱层内所有点,并取前较小的一半进行询问。这样:
- 如果该层有
3 个点,且只有一个点是有效的:- 第一次询问有
\frac{1}{3} 的概率确定,此时共询问1 次。 - 如果第一次没能确定,则第二次询问一定能确定,此时共询问
2 次。 - 单层总共期望询问的次数是
\frac{1}{3}\times1+\frac{2}{3}\times2=\frac{5}{3} 次,所有层加起来就是\frac{n}{3}\times\frac{5}{3}=\frac{5}{9}n 。
- 第一次询问有
- 类似的,如果该层有
5 个点,且只有一个点是有效的:- 第一次询问有
\frac{2}{5} 的概率范围内有点,此时只需要再询问一次即可确定,共询问2 次。 - 否则根据上面的结论大小为
3 的层中期望需要\frac{5}{3} 次询问,共询问\frac{8}{3} 次。 - 总共期望的询问次数是
\frac{2}{5}\times1+\frac{3}{5}\times\frac{8}{3}=2 次,所有层加起来就是\frac{n}{5}\times2=\frac{2}{5}n 。
- 第一次询问有
- 我们发现,
3 个点的层仍然是最坏的,但总共的次数由于随机化变为了期望的\frac{14}{9}n 次。 :::
D
自评:下位蓝~中位蓝。
没经历过真的体会不到把 Hint 部分的结论光速全观察出来了结果刚了 1h 不会的绝望……
不难注意到每个怪只会受到一次摔伤,且摔伤分为
于是我们对于第
- 直接打死,花费
h_i 次攻击; - 先摔(大于
1 格)再打死,花费\max\{h_i-i,0\} 次攻击,同时要求第i-1 个怪必须直接打死; - 先摔(
1 格)再打死,花费h_i-1 次攻击。
直接对着这个 DP 即可。第二种情况完全包含了第一种,所以可以直接无视第一种。
dp[0]=0;
dp[1]=a[1];
for(int i=2;i<=n;i++){
dp[i]=min(dp[i-1]+a[i]-1,dp[i-2]+a[i-1]+max(a[i]-i+1,0ll));
}
cout<<dp[n]<<endl;
E
自评:中位蓝~上位蓝。
很显然的,我们需要通过不超过
既然我们拆成若干条链,那得知道链是长什么样子的。
随便构造了若干棵树,我们发现如果假定了一个根,则执行完二操作后剩下的链可能是下面两种情况:
而第二种(上图右侧)又是由两个第一种(上图左侧)连到同一个父亲拼出来的。于是断点的策略就十分显然了:
- 递归求解;下面的“儿子”不考虑断掉的点。
- 如果递归完下面后这个点只有
0 或1 个儿子,那显然是符合第一种的,直接返回; - 如果递归完下面后这个点有两个儿子,那是符合第二种的,此时需要断掉这个点的父亲(如果有);
- 如果递归完下面后这个点有超过两个儿子,那需要将这个点断开。
其中有两个儿子的情况注意判断父亲是否已经被其他点断过了,没判会导致 WA on #2 case 652。
然后对于每个链找它的一端把这个链扫一遍就可以了。复杂度线性。
:::info[相似题目推荐]
P11017 Hide And Seek,也有我的题解。赶紧去点赞!
:::
ll T,n,u,v;
bool vis[200010],vis2[200010];
vector<ll> a[200010],ans1,ans2;
void dfs1(ll now,ll lst){
ll cnt=0;
for(int i=0;i<a[now].size();i++){
if(a[now][i]!=lst){
dfs1(a[now][i],now);
}
}
if(vis[now])return ;
for(int i=0;i<a[now].size();i++){
if(a[now][i]!=lst&&!vis[a[now][i]]){
cnt++;
}
}
if(cnt>2){
vis[now]=1;
ans2.push_back(now);
}
else if(cnt==2&&now!=lst){
if(!vis[lst])ans2.push_back(lst);
vis[lst]=1;
}
}
void dfs2(ll now,ll lst){
ans1.push_back(now);
vis2[now]=1;
for(int i=0;i<a[now].size();i++){
if(a[now][i]!=lst&&!vis[a[now][i]]){
dfs2(a[now][i],now);
}
}
}
int main(){
cin>>T;
while(T--){
ans1.clear();
ans2.clear();
cin>>n;
for(int i=1;i<=n;i++){
a[i].clear();
vis[i]=0;
vis2[i]=0;
}
for(int i=1;i<n;i++){
cin>>u>>v;
a[u].push_back(v);
a[v].push_back(u);
}
dfs1(1,1);
for(int i=1;i<=n;i++){
//找链的端点
if(vis2[i])continue;
if(vis[i]){
ans1.push_back(i);
}
else{
ll cnt=0;
for(int j=0;j<a[i].size();j++){
if(!vis[a[i][j]]){
cnt++;
}
}
if(cnt<=1){
dfs2(i,i);//扫链
}
}
}
cout<<ans1.size()+ans2.size()<<endl;
for(int i=0;i<ans2.size();i++){
cout<<"2 "<<ans2[i]<<endl;
}
for(int i=0;i<ans1.size();i++){
cout<<"1 "<<ans1[i]<<endl;
}
}
return 0;
}
F
自评:下位紫~中位紫。
显然,对于两个苦力怕,如果它们的爆炸区间都包含另一个的爆炸点,不能都炸掉(因为无论按什么顺序其中一个都会把另一个的爆炸点给炸死)。
于是有暴力 DP:设
于是暴力枚举
我们发现转移的过程是个区间最小值,所以我们可以考虑将目前炸死前
- 对于第一种,查
[i-e_i,i) 的最小值即可。 - 对于第二种,直接在这棵线段树上是难以完成的。但是,我们发现,我们可以在前
i-e_i 个苦力怕处理完的时候就进行查询,这样只会查询到在i 的左端点往左的j 。我们对于每个位置开一个vector预处理出该位置处理完后要更新的dp 值,就可以不消耗额外的时间了。
然后就可以实现快速的转移了。输出答案的部分就是个普通的 DP 记录路径,维护个前驱数组,转移的同时也把右端点是哪个苦力怕炸的挂到线段树上一起维护,最后按
虽然只有不到 3KB,但细节真的很多。。。一共用了一下午加一晚上才处理完的。。。
struct node{
ll ls,rs,mn,lst;
};
struct node2{
ll mn,lst;
};
bool operator <(node2 x,node2 y){
return x.mn<y.mn;
}
node t[2000010];//这边稍微开大一点,下标从 0 开始方便处理
ll T,n,a[500010],dp[500010],lst[500010],rt,p=1;
vector<ll> res[500010],ans;//记录每个位置计算完后要更新的位置
bool cmp(ll x,ll y){
return a[x]<a[y];
}
void build(ll now,ll l,ll r){
if(l==r){
if(l==0)t[now].mn=0;
else t[now].mn=1000000000000000000ll;
t[now].lst=0;
return ;
}
ll mid=(l+r>>1);
t[now].ls=p++;
build(t[now].ls,l,mid);
t[now].rs=p++;
build(t[now].rs,mid+1,r);
t[now].mn=min(t[t[now].ls].mn,t[t[now].rs].mn);
}
void upd(ll now,ll l,ll r,ll pos,ll x,ll y){
if(l==r){
if(t[now].mn>x||t[now].mn==x&&t[now].lst>y){
t[now].mn=x;
t[now].lst=y;
}
return ;
}
ll mid=(l+r>>1);
if(pos<=mid){
upd(t[now].ls,l,mid,pos,x,y);
}
else{
upd(t[now].rs,mid+1,r,pos,x,y);
}
if(t[t[now].ls].mn<t[t[now].rs].mn){
t[now].mn=t[t[now].ls].mn;
t[now].lst=t[t[now].ls].lst;
}
else{
t[now].mn=t[t[now].rs].mn;
t[now].lst=t[t[now].rs].lst;
}
}
node2 query(ll now,ll l,ll r,ll L,ll R){
if(l>=L&&r<=R){
return {t[now].mn,t[now].lst};
}
ll mid=(l+r>>1);
if(R<=mid){
return query(t[now].ls,l,mid,L,R);
}
else if(L>mid){
return query(t[now].rs,mid+1,r,L,R);
}
else{
return min(query(t[now].ls,l,mid,L,R),query(t[now].rs,mid+1,r,L,R));
}
}
int main(){
cin>>T;
while(T--){
cin>>n;
rt=p++;
build(rt,0,n);
for(int i=1;i<=n;i++){
cin>>a[i];
res[i].clear();
if(a[i])res[max(i-a[i],0ll)].push_back(i);
dp[i]=1000000000000000000ll;
}
for(int i=1;i<=n;i++){
if(a[i]){
node2 tmp=query(rt,0,n,max(i-a[i],0ll),max(i-1,0));
if(tmp.mn+1<dp[i]||tmp.mn+1==dp[i]&&tmp.lst<lst[i]){
dp[i]=tmp.mn+1;
lst[i]=tmp.lst;
}
upd(rt,0,n,min(i+a[i]-1,n),dp[i],i);
}
for(int j=0;j<res[i].size();j++){
node2 tmp=query(rt,0,n,max(res[i][j]-a[res[i][j]],0ll),min(res[i][j]+a[res[i][j]]-1,n));
dp[res[i][j]]=tmp.mn+1;
lst[res[i][j]]=tmp.lst;
}
}
if(query(rt,0,n,n,n).mn>999999999999999999ll){
cout<<-1<<endl;
continue;
}
ans.clear();
ll tmp=query(rt,0,n,n,n).lst;
while(tmp){
ans.push_back(tmp);
tmp=lst[tmp];
}
sort(ans.begin(),ans.end(),cmp);
cout<<ans.size()<<endl;
for(int i=0;i<ans.size();i++){
cout<<ans[i]<<' ';
}
cout<<endl;
}
return 0;
}