B
可以把一个区间的数字都变成出现次数较多的元素,问把整个数组变成相同元素,最少操作几次。
首先可以注意到只有01两种元素,那么长度为奇数的数组,01个数一定不等,也就是01一定有一个比另一个更多,可以执行操作变成同一个,因此长度为奇数答案至多为1
长度为偶数时,如果01个数不同,也是一次操作。如果01个数相同,我们可以取长度为n-1的子数组,它的长度是奇数,一定能通过一次操作变成一样的。这样的子数组一共有四个,我们看这四个情况,变完之后剩下的那一个元素和变完的元素是否相同,如果相同就是一次操作。如果不同还需要一次把剩下的拿一个元素变了,就是两次操作
当然需要注意初始全都相同就不用操作
void solve(){
string s;
cin>>n>>s;
if(n==1){
cout<<0;
}
else if(n==2){
if(s[0]!=s[1]){
cout<<-1;
}
else{
cout<<0;
}
}
else{
if(n%2){
int cnt=0;
for(char c:s){
if(c=='0'){
cnt++;
}
}
if(cnt==n||cnt==0){
cout<<0;
}
else{
cout<<1;
}
}
else{
int cnt0=0,cnt1=0;
rep(i,0,n-1){
if(s[i]=='0')cnt0++;
else cnt1++;
}
if(cnt0==0||cnt0==n){
cout<<0;
return;
}
if(cnt0!=cnt1){
cout<<1;
return;
}
if(s[n-1]=='1')cnt1--;
else cnt0--;
if(cnt1>cnt0&&s[n-1]=='1')cout<<1;
else if(cnt0>cnt1&&s[n-1]=='0')cout<<1;
else{
int cnt0=0,cnt1=0;
rep(i,1,n-1){
if(s[i]=='0')cnt0++;
else cnt1++;
}
if(cnt1>cnt0&&s[0]=='1')cout<<1;
else if(cnt0>cnt1&&s[0]=='0')cout<<1;
else cout<<2;
}
}
}
}
C
滑窗
把数组分成三个子数组,要求中间的子数组元素和比另外两个都大,问有多少种划分。
划分成三段,其实等价于找一个子数组,只是要求剩下的前缀和后缀不能为空。这种统计合法子数组个数可以考虑滑窗。滑窗内时中间子数组,滑窗左右分别是另外两个子数组。由于元素是正的,我们可以对于每个r,找到最靠右的l,这个l左边的位置也都可以作为l,因为往左移动的话中间子数组会变大,左侧子数组会变小,如果原本可以的话,l往肯定也可以
需要注意的是可能l滑到头了也不合法,所以在加答案的时候,要判断一下l是否合法
void solve(){
cin>>n;
vi a(n+1);
rep(i,1,n)cin>>a[i];
int b1=a[1],b2=a[2],b3=0;
rep(i,3,n)b3+=a[i];
int j=2,ans=0;
rep(i,3,n-1){
b2+=a[i];
b3-=a[i];
while(j<i&&b2>b3&&b2>b1){
b1+=a[j];
b2-=a[j];
j++;
}
if(b2+a[j-1]>b3&&b2+a[j-1]>b1-a[j-1])ans+=j-2;
}
cout<<ans<<'\n';
}
D
奇数和奇数,偶数和偶数相连代价a,奇数和偶数相连代价b。问所有点联通的最小代价。
这题比较坑人的点在于ab可以是负的,所以联通之后为了代价更小,还可以加边。
如果ab都负,把能加的边都加上,也就是任意一个奇数和任意一个偶数奇数/偶数内部任意两点,都可以连线
如果a正b负。把奇数偶数之间连满,此时一定就联通了不用连别的。注意可能所有数都是奇数/偶数,那么只能用a尽可能少连,也就是连n-1条
如果a负b正,奇数内部,偶数内部练满,如果奇数和偶数都至少有一个,需要一个b联通奇数和偶数
如果ab都正,首先肯定需要一个b连接一个奇数和一个偶数,接下来每个数都可以任意选连接到奇数还是偶数,那么根据ab大小来选择
void solve(){
int a,b,n;
cin>>n>>a>>b;
int cnt0=0,cnt1=0;
rep(i,1,n){
int t;
cin>>t;
if(t%2)cnt1++;
else cnt0++;
}
int ans=0;
if(a<=0&&b<=0){
ans+=cnt0*cnt1*b;
ans+=cnt0*(cnt0-1)/2*a+cnt1*(cnt1-1)/2*a;
}
else if(a<=0){
ans+=cnt0*(cnt0-1)/2*a+cnt1*(cnt1-1)/2*a;
if(cnt1&&cnt0)ans+=b;
}
else if(b<=0){
if(cnt0==0||cnt0==n)ans+=(n-1)*a;
else ans+=cnt0*cnt1*b;
}
else{
if(cnt0==0||cnt0==n)ans+=(n-1)*a;
else{
ans+=b;
if(a<=b)ans+=(n-2)*a;
else ans+=(n-2)*b;
}
}
cout<<ans<<'\n';
}
E
可以删一个子树,让支撑节点最大化。支撑结点的定义是,所有祖先的权值和大于等于自己,所有子孙的权值和小于等于自己。
显然可以先求出初始的支撑节点个数,然后dfs的过程中,遍历所有子树,也就是枚举删除子树。然后计算删除每个子树的情况下的支撑节点个数,取max
那么怎么计算删除一棵子树后支撑节点的变化?首先能影响到的肯定只有当前结点的祖先,然后再这些祖先节点中,满足a[i]>=sum[i]-sum[j],sum[i]表示i的子孙的权值和,j是当前枚举删除的子树根节点,a是点权
的点,会成为新的支撑节点
变形一下,就是要找祖先节点中,满足sum[i]-a[i]<=sum[j]
的点有多少个。这是个区间查询,并且我们dfs的过程中区间点数会变,也就是是一个动态区间查询,可以用平衡树/树状数组/线段树,后两个由于ai很大需要离线
int t[N];
int lowbit(int x){
return x&(-x);
}
void add(int x,int k){
while(x<=m){
//cout<<x<<'\n'
t[x]+=k;
x+=lowbit(x);
}
}
int ask(int x){
int res=0;
while(x>0){
//cout<<x<<'\n';
res+=t[x];
x-=lowbit(x);
}
return res;
}
void solve(){
cin>>n;
vi a(n+1),s(n+1);
vector<vi>g(n+1);
rep(i,1,n)cin>>a[i];
rep(i,1,n){
int t;
cin>>t;
if(!t)continue;
g[t].pb(i);
}
auto &&dfs=[&](auto &&dfs,int u)->void{
s[u]=a[u];
for(int v:g[u]){
dfs(dfs,v);
s[u]+=s[v];
}
};
int tot=0;
vi all,cnt(n+1);
auto &&dfs0=[&](auto &&dfs0,int u,int up)->void{
if(up>=a[u]&&a[u]>=s[u]-a[u])tot++,cnt[u]=1;
for(int v:g[u]){
dfs0(dfs0,v,up+a[u]);
cnt[u]+=cnt[v];
}
};
dfs(dfs,1);
dfs0(dfs0,1,0);
rep(i,1,n){
all.pb(s[i]);
all.pb(s[i]-2*a[i]);
}
sort(all.begin(),all.end());
all.erase(unique(all.begin(),all.end()),all.end());
m=all.size();
unordered_map<int,int>lisan;
rep(i,0,m-1){
lisan[all[i]]=i+1;
}
int ans=0;
//cout<<tot<<'\n';
auto &&dfs1=[&](auto &&dfs1,int u,int up)->void{
ans=max(ans,ask(lisan[s[u]])-cnt[u]);
//cout<<ask(lisan[s[u]])<<' '<<cnt[u]<<' '<<u<<'\n';
if(up>=a[u])add(lisan[s[u]-2*a[u]],1);
for(int v:g[u]){
dfs1(dfs1,v,up+a[u]);
}
if(up>=a[u])add(lisan[s[u]-2*a[u]],-1);
};
dfs1(dfs1,1,0);
cout<<ans+tot<<'\n';
}
F
很妙的一道题,重点是优化思路
对于每个前缀,求他的所有圆排列中,逆序对个数最小值。一个显然的思路是,枚举前缀长度,对于每个前缀,枚举所有圆排列,枚举的过程实际上就是每次都把头部元素放到最后,这一步对逆序对的影响可以用ds维护,实际上就是求大于头部元素的元素个数,这样复杂度是
但是看数据,我们要实现。这就需要一些智慧了:实际上把头部元素移动到末尾,对逆序对的影响,我们不一定要用ds,这一步实际上就是要知道比a[1]大的元素有多少个(逆序对会增加多少),以及比a[1]小的元素有多少个(逆序对会减少多少),这个东西我们可以开个数组维护:w[i]表示,考虑当前前缀,比i大的元素个数-比i小的元素个数。这个东西很好维护,我们枚举前缀的时候,每加入一个元素,就可以把比它小的元素的w[i]++,比他大的元素的w[i]--
void solve(){
cin>>n;
vi a(n+1);
rep(i,1,n)cin>>a[i];
vi w(n+1);
int ans=0;
rep(i,1,n){
rep(j,1,a[i])w[j]++;
rep(j,a[i],n)w[j]--;
rep(j,1,i)ans+=a[j]>a[i];
int cur=0;
int mn=0;
rep(j,1,i){
cur+=w[a[j]];
mn=min(mn,cur);
}
cout<<ans+mn<<' ';
}
}