【学习笔记】2021.10.9 - zhengru IOI 七连测 Day6
T1聚会
正解
思路
-
简单题,开栈暴力存储每个 1 的位置,然后暴力向两边拓展更新答案即可。
-
当拓展到另一个 1 时应该立即停止,因为再进行拓展一定会多上一个 两个 1 之间的距离,一定不会更优,没有意义。
-
至此,每一段区间最多被遍历两次,复杂度 \(O(Tn)\) ,可以通过。
代码
#include<iostream>
#include<cstdio>
#include<algorithm>
#include<cstdlib>
using namespace std;
int T,n,ans[500233],stk[500233],tot,Case=0;
char ch[500233];
long long ouout;
inline int R(){
int x=0,f=1;char c='c';
while(c>'9'||c<'0'){f=f*(c=='-'?-1:1);c=getchar();}
while(c<='9'&&c>='0'){x=x*10+c-'0';c=getchar();}
return x*f;
}
inline void gechch(){
char c='c';int pos=0;
while(c!='1'&&c!='0') c=getchar();
while((++pos)&&(c=='1'||c=='0')){ans[pos]=((c=='1')?0*(stk[++tot]=pos):1919810);c=getchar();}
return;
}
int main(){
T=R();
while(T--){
tot=0;ouout=0;
n=R();
gechch();
for(register int i=1;i<=tot;++i){
for(register int j=stk[i]-1;ans[j];--j){if(ans[j]<=stk[i]-j) break;ans[j]=stk[i]-j;}
for(register int j=stk[i]+1;ans[j];++j) ans[j]=j-stk[i];
}
for(register int i=1;i<=n;++i) ouout+=ans[i];
printf("Case #%d: %lld\n",++Case,ouout);
}
return 0;
}
T2 跳房子
正解
思路
-
我们发现,对于 \(i<j\) ,如果 \(x_i\) 与 \(x_j\) 存在倍数关系,则因为 \(x_i<x_j\) ,容易得出 \(x_j\) 至少是 \(x_i\) 的两倍。
-
我们记录一下 \(x_i\) 在二进制下的最高位,表示为 \(f(x_i)\) ,则根据上一条,容易得知,如果 \(x_i\) 和 \(x_j\) 存在倍数关系,则 \(f(x_i)+1 \le f(x_j)\) 。
-
也就是说,不应该使得最高次幂连续递增 1 的四个数分配给同一个人。
-
换句话说,如果 \((i,j)\) 想分配给一个人,需要满足 \(\left\lfloor\frac{f(x_i)}{4}\right\rfloor=\left\lfloor\frac{f(x_j)}{4}\right\rfloor\) 。
-
那么,不满足的数对怎么办呢?
-
显然,把这些扔给第二个人即可。但是为了保证合法性,仍需要满足一个式子: \(\left\lfloor\frac{f(x_i)}{16}\right\rfloor=\left\lfloor\frac{f(x_j)}{16}\right\rfloor\) 。
-
于是,剩下的扔给第三个人即可。
-
为什么?
-
显然,剩下的数对每隔 \(2^16\) 会产生一次,而 \(x \le 10^18\) ,即 \(x \le 2^64-1\),即 \((x)_2\) 的位数 \(\le 63\) ,又因为 \(\frac{63}{16} \le 3\) ,所以易得剩下的全给第三个人仍然合法。
代码
#include<iostream>
#include<cstdio>
#include<algorithm>
using namespace std;
int n,num[1234];
long long inn;
inline int chul(long long num){
for(register int i=63;i>=0;--i) if((num>>i)&1) return i+1;
return 0;
}
inline int ggeett(int a,int b){
if((num[a]/4)==(num[b]/4)) return 1;
if((num[a]/16)==(num[b]/16)) return 2;
return 3;
}
int main(){
scanf("%d",&n);
for(register int i=1;i<=n;++i){
scanf("%lld",&inn);
num[i]=chul(inn);
}
for(register int i=1;i<n;++i){
for(register int j=1;j<=i;++j){
printf("%d ",ggeett(j,i+1));
}
printf("\n");
}
return 0;
}
T4 辣椒
Subtask 1
思路
-
\(O(n^2)\) 枚举选择哪条边,再 \(O(n)\) 处理子树大小即可。
-
总复杂度 \(O(n^3)\)
Subtask 2
思路
- 预处理以一个点为根的子树大小 \(S(x)\) ,对于一对 \(x,y\) :
-
当 y 是 x 的祖先时,三个连通块分别是 \(S(x),S(y)-S(x),n-S(y)\) 。
-
当 x 和 y 无祖先关系时,三个连通块分别是 \(S(x),S(y),n-S(x)-S(y)\) 。
-
如果 x 是 y 的祖先,那么当 x 和 y 位置互换时还会计算一遍,没有必要考虑。
-
总复杂度 \(O(n^2)\) 。
正解
思路
-
怎么判断是祖先还是兄弟?一提到这个,我们就会马上想到 Tarjan !
-
没错,这里也是这样做的。
-
一个点 DFS 之前,将其扔进集合 A 去,DFS 后再扔到集合 B 里!
-
那么对于当前的点 x ,如果 y 在 A 里,则按第一种情况处理,否则第二种。
-
明白了大致思路,现在开始考虑怎么处理。
-
我们选择使用 \(multiset\) 存储 A 和 B 集合,因为这个东西可以使序列保持有序,当然,一提到有序基本就和二分离不开了,实际上也就是用了二分,后面会说。
-
继续沿着前面的思路谈,其实,对于上面的两种情况,如果答案最优,那么 y 最接近的值应该分别对应着 \(\frac{n-S(x)}{2}+S(x)\) 和 \(\frac{n-S(x)}{2}\) 。(就是在蟹老板给的式子的基础上再移移项就好了,毕竟 y 和某东西的差越小越好不久是 y 和某东西越接近越好嘛(当然蟹老板的式子我自己肯定推不出来啊QWQ))
-
所以,我们要在这两个集合中分别二分找到最接近这两个数的 y !
-
但是,二分有一定的局限性。multiset 支持的只有祖传的 \(upper_bound\) 和 \(lower_bound\) ,只能找到偏大的值,而我们需要的是 绝对值最小,所以如果不是集合最靠前的值,都需要考虑上一个值,因为不免会有一种情况使得找到的那个偏大的数的偏差比偏小的数的偏差要大,这个时候舍近求远就 GG 了。
-
然后,处理出来三个连通块的大小,直接更新答案即可(当然能不能更新上还要看它优不优秀了
-
总复杂度 \(O(n\log n)\) ,可以过题。
代码
#include<iostream>
#include<cstdio>
#include<algorithm>
#include<set>
using namespace std;
const int nan_hen_hen_aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa(1919810);
int siz[200233];
int n;
int ans=nan_hen_hen_aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa;
struct edge{
int to,nxt;
}ed[400233];
int head[200233],cnt;
multiset<int> DOING,DONE;
inline void addd(int fr,int to){
ed[++cnt].to=to;
ed[cnt].nxt=head[fr];
head[fr]=cnt;
return;
}
int gesize(int now,int fa){
siz[now]=1;
for(register int i=head[now];i;i=ed[i].nxt){
int nexet=ed[i].to;
if(nexet==fa) continue;
siz[now]+=gesize(nexet,now);
}
return siz[now];
}//预处理子树大小
inline void bestans(int x,int y){
int z=n-x-y;
if(z>0) ans=min(ans,max({x,y,z})-min({x,y,z}));
return;
}//更新最优答案
void DFS(int now,int fa){
if(DOING.size()){
auto it=DOING.lower_bound((n-siz[now])/2+siz[now]);
if(it!=DOING.end()) bestans(siz[now],*it-siz[now]);
if(it!=DOING.begin()) bestans(siz[now],*(--it)-siz[now]);
}//处理祖先关系
if(DONE.size()){
auto it=DONE.lower_bound((n-siz[now])/2);
if(it!=DONE.end()) bestans(siz[now],*it);
if(it!=DONE.begin()) bestans(siz[now],*(--it));//防止因只能找偏大的值而GG,体现了OI的科学性、准确性,使代码更加生动形象(上同)
}//处理兄弟关系
if(now-1) DOING.insert(siz[now]);//当然,根节点如果选上直接全部木大,这个不需要考虑了
for(register int i=head[now];i;i=ed[i].nxt){
int nexet=ed[i].to;
if(nexet==fa) continue;
DFS(nexet,now);
}
if(now-1) DOING.erase(DOING.find(siz[now])),DONE.insert(siz[now]);//处理完了!
return;
}
int main(){
scanf("%d",&n);
for(register int i=1;i<n;++i){
int u,v;
scanf("%d%d",&u,&v);
addd(u,v);addd(v,u);
}
gesize(1,0);
DFS(1,0);
printf("%d\n",ans);
return 0;
}