Educational Codeforces Round 126 (Rated for Div. 2) A-F

A题:Array Balancing(迷惑性很强)(GOOD)

在这里插入图片描述在这里插入图片描述

  • 迷惑性强
    • 下界:每一对取 m i n { ∣ a [ i ] − a [ i − 1 ] ∣ + ∣ b [ i ] − b [ i − 1 ] ∣ , ∣ a [ i ] − b [ i − 1 ] ∣ + ∣ b [ i ] − a [ i − 1 ] ∣ } min\{|a[i]-a[i-1]|+|b[i]-b[i-1]|,|a[i]-b[i-1]|+|b[i]-a[i-1]|\} min{a[i]a[i1]+b[i]b[i1],a[i]b[i1]+b[i]a[i1]}
    • 一定可以构造出该下界:后面只需看前面i-1是否交换。
  • 或考察最优解的性质(贪心选择):相邻之间的差和一定最小。如果不这样统一交换后面所有得到更优的解。
  • 最优子结构(显然)

int main()
{
    int t;cin>>t;
    for(int i = 0;i<t;i++)
    {
        int n;cin>>n;
        int a[n];
        int b[n];
        for(int k = 0;k<n;k++) cin>>a[k];
        for(int k = 0;k<n;k++) cin>>b[k];
        ll ans = 0;
        for(int k = 1;k<n;k++) 
        {
            int c = min(abs(a[k]-a[k-1])+abs(b[k]-b[k-1]),abs(a[k]-b[k-1])+abs(b[k]-a[k-1]));
            ans+=(ll)c;
        }
        cout<<ans<<endl;
    }
    system("pause");
}


B题-Getting Zero

在这里插入图片描述在这里插入图片描述

思路

  • 正向BFS找最短路困难,考虑反向BFS找最短路(避免重复从开始结点搜索)
  • 根据题目条件
    在这里插入图片描述
    可以得出目标到开始结点每一条边什么时候存在。
    算法分析:每个结点至多入队一次,出队一次。循环总公运行32768次。
void solve(int dist[])
{   
    dist[0]=0;
    queue<int> q; q.push(0);
    int count = 0;
    while(!q.empty()) //循环至多运行32768次
    {
        count++;
        int v = q.front();
        q.pop();
        if(v%2==0&&dist[v/2]==-1) dist[v/2]=dist[v]+1,q.push(v/2);
        if(v%2==0&&dist[(v+32768)/2]==-1) dist[(v+32768)/2]=dist[v]+1,q.push((v+32768)/2);
        if(dist[(v+32767)%32768]==-1) dist[(v+32767)%32768]=dist[v]+1,q.push((v+32767)%32768);
    }
    cout<<count<<endl;
}

int main()
{
    int dis[32768];
    memset(dis,-1,32768*sizeof(int));
    solve(dis);
    int n;cin>>n;
    for(int j = 0;j<n;j++)
    {
        int a;
        cin >> a;
        cout<<dis[a];
        if(j<n-1) cout<<" ";
    }
    system("pause");
}

C题 Water the Trees

在这里插入图片描述
在这里插入图片描述

思路

  • 只需搜索 m a x h e i g h t maxheight maxheight m a x h e i g h t + 1 maxheight+1 maxheight+1的情况。是因为只需要把奇数用1弥补(必要条件也是充分条件)
  • 二分查找最小天数即可。

int main()
{
    int t;
    cin>>t;
    for(int i = 0;i<t;i++)
    {
        int n;cin>>n;
        int h[n];
        for(int k =0;k<n;k++) cin>>h[k];
        ll sum = 0;
        ll maxm = -1;
        ll c_odd = 0;   //需要的奇数
        ll c_even = 0;  //需要的偶数
        for(int k = 0;k<n;k++)
        {
            sum+=(ll)h[k];
            maxm=max(maxm,(ll)h[k]);
            if(h[k]%2==0) c_even++; 
            else c_odd++;
        }
        //同样高度为maxm
        ll remain = maxm*(ll)n-sum;
        if(maxm%2!=0)
        {
            ll temp = c_odd;c_odd=c_even;c_even=temp;
        }
        ll low = 0,high = 2*remain+1;
        while(low<high)
        {
            ll mid = (low+high)>>1;
            if(!(mid-mid/2>=c_odd&&(mid/2*3+(mid%2)) >= remain)) low=mid+1;   //如果不满足条件
            else high=mid;
        }
        ll ans = low;
        ll temp = c_odd;c_odd=c_even;c_even=temp;
        remain = (maxm+1)*(ll)n-sum;
        low = 0,high = 2*remain+1;
        while(low<high)
        {
            ll mid = (low+high)>>1;
            if(!(mid-mid/2>=c_odd&&(mid/2*3+(mid%2))>=remain)) low=mid+1;   //如果不满足条件
            else high=mid;
        }
        ans = min(ans,low);
        cout<<ans<<endl;
    }
    system("pause");
}

D题Progressions Covering在这里插入图片描述在这里插入图片描述

思路

  • 从最后一个点往前贪心
  • 关注当前窗口何时终止(以curs[i]记录),以维护每个点应该被减去的值predec
#include<iostream>
#include<vector>
#include<string>
#include<set>
#include<algorithm>
#include<map>
#include<queue>
#include <chrono>
#include<math.h>
#include<unordered_map>
#include <bits/stdc++.h>
using namespace std;
using namespace std;
const int N = 1e5+5;
const int S =  500;
const long long mod = 1e9+7;
typedef long long ll;


int main()
{
    int n,k; cin>>n>>k;
    ll b[n];
    for(int i = 0;i<n;i++) cin>>b[i];
    int curs[n]; //在何时终止减去该值
    memset(curs,-1,n*sizeof(int));
    ll dec[n];  
    memset(dec,0,n*sizeof(ll));
    ll sum = 0;      //作为等差数列应该减去的值
    ll pre_dec = 0;  //前一个数字减去的值
    ll ans = 0;
    for(int i = n-1;i>=0;i--)
    {
        ll c = b[i];
        pre_dec -= sum;  //当前数值应减去的数字
        if(c - pre_dec > 0)
        {
            ll c_c = (ll)ceil((double)(c - pre_dec)/(double)(min(k,i+1)));
            ans+=c_c;
            sum+=c_c;
            dec[i]=c_c;
            pre_dec += c_c * min(k,i+1);
            int index = max(0,i-k+1);
            curs[index] = i;  //在index停止减去  
        }
        if(curs[i]!=-1) {sum-=dec[curs[i]];pre_dec -=dec[curs[i]];}   //下一个数不该此等差数列中的结果
    }
    cout << ans << endl;
    system("pause");
    return 0;
}


E题(Narrow Components)

在这里插入图片描述
在这里插入图片描述

思路

我的思路(BAD)

  • 维护每个前缀连通分量的个数
  • 分类讨论。尤其需要注意一列中中间堵住,两边不堵住的情况,连通分量个数有可能会减少
#include<iostream>
#include<vector>
#include<string>
#include<set>
#include<algorithm>
#include<map>
#include<queue>
#include <chrono>
#include<math.h>
#include<unordered_map>
#include <bits/stdc++.h>
using namespace std;
using namespace std;
const int N = 1e5+5;
const int S =  500;
const long long mod = 1e9+7;
typedef long long ll;

int getpre(int l,int r,int pre[])
{
    if(l==0) return pre[r];
    return pre[r]-pre[l-1];
}
int main()
{
    int n; 
    cin >> n;
    string a[3];
    cin >> a[0]; cin>>a[1]; cin>>a[2];
    //1 free cell,0 taken cell
    int pre1[n],pre2[n];
    pre1[0] = a[0][0]-'0'; pre2[0] = a[2][0]-'0';
    for(int i = 1;i<n;i++) {pre1[i] = pre1[i-1]+(a[0][i]-'0');pre2[i] = pre2[i-1]+(a[2][i] - '0');}
    //一列三个都是空闲的下标
    vector<int> em;
    for(int i = 0;i<n;i++) 
    {
        if(a[0][i] == '1'&&a[1][i] == '1'&&a[2][i] == '1') em.push_back(i);
    }
    int pref[n];   //前缀连通分量个数
    if(a[1][0] == '0') pref[0]=(a[0][0]-'0') + (a[2][0] - '0'); else pref[0]=1;
    for(int i = 1;i<n;i++)  //
    {
        if(a[1][i] == '0') //中间堵住
        {
            pref[i]=pref[i-1]+((!(a[0][i-1]-'0') && (a[0][i]-'0'))+(!(a[2][i-1]-'0') && (a[2][i] - '0')));
        }
        else   //中间没有堵住
        {
            if(a[1][i-1]=='1') pref[i]=pref[i-1];
            else
            {
                if(a[2][i]=='0')  //下面的堵住
                {
                    if(a[0][i]=='0' || a[0][i]=='1' && a[0][i-1]=='0') pref[i]=pref[i-1]+1;
                    else pref[i]=pref[i-1];
                }
                else if(a[0][i] == '0')  //上面的堵住
                {
                    if(a[2][i]=='0' || a[2][i]=='1' && a[2][i-1]=='0') pref[i]=pref[i-1]+1;
                    else pref[i]=pref[i-1];
                }
                else    //三个都没堵住
                {
                    if(a[0][i-1]=='0'&&a[2][i-1]=='0') pref[i]=pref[i-1]+1;
                    else if(a[0][i-1]=='1'&&a[2][i-1]=='1')
                    {
                        //搜索最后一个小于i-1下标的em
                        int l = 0,r = em.size();
                        while(l<r)
                        {
                            int m = (l+r)>>1;
                            if(em[m]<i-1) l=m+1; else r = m;
                        }
                        l--;
                        if(l>=0&&l<em.size()&&getpre(em[l],i,pre1)==i-em[l]+1&&getpre(em[l],i,pre2)==i-em[l]+1)
                        {
                            pref[i]=pref[i-1];
                        }
                        else  pref[i]=pref[i-1]-1;       //本来就存在于一个CC中
                    }
                    else pref[i]=pref[i-1];
                }
            }
        }
    }
    int q;cin>>q;
    for(int i = 0;i<q;i++)
    {
        int l,r;cin>>l>>r;
        l--;r--;
        if(l==0) cout<<pref[r]<<endl;
        else
        {
            if(a[0][l] == '0' &&  a[1][l] == '0' && a[2][l] == '0') cout << pref[r]-pref[l]<<endl;
            else
            {
                if(a[1][l] == '1') cout<<pref[r]-pref[l]+1<<endl;
                else   //l中间的被堵住
                {
                    if(a[0][l]=='1'&&a[2][l]=='1')
                    {
                        int l1 = 0,r1 = em.size(),l2=0,r2 = em.size();
                        //找最后一个小于l的
                        while(l1<r1)
                        {
                            int m = (l1+r1)>>1;
                            if(em[m]<l) l1=m+1; else r1 = m;
                        }
                        l1--;
                        while(l2<r2)
                        {
                            int m = (l2+r2)>>1;
                            if(em[m]<=l) l2=m+1; else r2=m;
                        }
                        bool flag1 = false,flag2 = false;  //前缀中是否位于同一个连通分量
                        if(l1>=0&&l1<em.size()&&getpre(em[l1],l,pre1)==l-em[l1]+1&&getpre(em[l1],l,pre2)==l-em[l1]+1)
                            flag1 = true;
                        if(l2>=0&&l2<em.size()&&em[l2]<=r&&getpre(l,em[l2],pre1)==em[l2]-l+1&&getpre(l,em[l2],pre2)==em[l2]-l+1)
                            flag2 = true;
                        if(flag1&&flag2)cout<<pref[r]-pref[l]+1<<endl;
                        else cout<<pref[r]-pref[l]+2<<endl;
                    }
                    else cout<<pref[r]-pref[l]+1<<endl;
                }   
            }
        }
    }
    system("pause");
}

官方解法(待补充)

F题 (Teleporters)GOOD 思维量极大

在这里插入图片描述
在这里插入图片描述

思路

  • 若固定点 b 1 , b 2 , ⋯ b n b_1,b_2,\cdots b_n b1,b2,bn,一定是从 b 1 b_1 b1跳到 b 2 b_2 b2,从 b 2 b_2 b2跳到 b 3 b_3 b3…从 b n − 1 b_{n-1} bn1跳到 b n b_n bn.以此类推。否则会产生多余的交叉项。

错误的贪心策略

  • 每次选择最大的段长度。选择其中点的位置作为添加点
    • 似乎每次能量减少得最多
    • 但是不能证明贪心选择得正确性(即不能证明一定存在一个最优解包含该种选择)

正解

最优解满足的条件
  • 最优解符合如下形式:若在 0 − a 1 0-a_1 0a1间插入 k 1 k_1 k1个点,在 a 1 − a 2 a_1-a_2 a1a2之间插入 k 2 k_2 k2个点,在 a 2 − a 3 a_2-a_3 a2a3之间插入 k 3 k_3 k3个点…以此类推。对每个区间而言,其插入点后形成的段都会是大致等间距的。即对每个区间 a i − 1 ∼ a i a_{i-1}\sim a_i ai1ai,插入点后划分的段长度至多相差1.要么为 ⌊ a i − a i − 1 k i + 1 ⌋ \lfloor \frac{a_i - a_{i-1}}{k_i +1} \rfloor ki+1aiai1,要么为 ⌈ a i − a i − 1 k i + 1 ⌉ \lceil \frac{a_i - a_{i-1}}{k_i +1} \rceil ki+1aiai1
在满足最优解条件的情况下搜索最优解
  • f ( x , k ) f(x,k) f(x,k)为将长度为x的区间,使用k个中间点划分(即形成k+1段)后,从该区间开始传递到末尾需要的能量。由于需考虑对单个区间增加一个点后的变化情况,因此考虑增加点后该区间能量的变化 f ( x , k ) − f ( x , k + 1 ) f(x,k) - f(x,k+1) f(x,k)f(x,k+1).关键步骤:
    f ( x , k ) − f ( x , k + 1 ) ≥ f ( x , k + 1 ) − f ( x , k + 2 ) f(x,k) - f(x,k+1) \geq f(x,k+1) - f(x,k+2) f(x,k)f(x,k+1)f(x,k+1)f(x,k+2)
    当允许每个划分的段的区间长度不是整数时,该结论很容易证明。说明对于任意 a i ∼ a i + 1 a_i \sim a_{i+1} aiai+1区间而言,每增加一个点带来的增量大小是单调不增的
    在这里插入图片描述如图所示,每个单元格定义为在当前区间添加一个点后,该区间总能量(亦即总能量)的变化情况。可将每一列视作一个降序数组。问题转换为将每个降序数组进行由大到小归并后,取最少的数使其和达到给定的阈值。在本问题中,该阈值就是能量的减少量。

  • 如图所示:由此带来一种类似归并排序的思路:遍历所有 a i , a i + 1 a_i,a_{i+1} ai,ai+1区间,并设当前区间已经有 k i k_i ki个点,每次选择使得 f ( a i + 1 − a i , k i ) − f ( a i + 1 − a i , k i + 1 ) f(a_{i+1}-a_{i},k_i) - f(a_{i+1}-a_{i},k_i+1) f(ai+1ai,ki)f(ai+1ai,ki+1)值最大对应的区间, 在其上面加上一个点,并重新计算该区间的能量。并维护能量总和,直到能量总和小于等于阈值m为止。

    • 但是复杂度还不可以接受
    • 我们还忽略了什么信息?
  • 如果在上述问题中,如果我们还知道每个数组的前缀和(或在常数时间内获取前缀和),可以通过二分套二分的思路进行优化。

    • 最优解满足:所有数组中取到的值都大于等于某个阈值 C C C。因此考虑二分这个阈值C。
    • 对于固定的阈值C,对每一列由于可以常数获取上述表格中每一列的前缀和,因此可以通过 O ( n l o g L ) O(nlogL) O(nlogL)判定所有选择的元素和是否满足条件
    • 当二分得到这个阈值C后,由于C+1不满足条件,C是满足条件的最大数,所有数组中取到的数都需要严格大于等于C,且每个数组中大于等于C的元素都被取走。因此还需要根据m 去除一些严格等于C的元素,以使得最终选择的元素个数最少
#include<iostream>
#include<vector>
#include<string>
#include<set>
#include<algorithm>
#include<map>
#include<queue>
#include <chrono>
#include<math.h>
#include<unordered_map>
#include <bits/stdc++.h>
using namespace std;
using namespace std;
const int N = 1e5+5;
const int S =  500;
const long long mod = 1e9+7;
typedef long long ll;

ll eval(ll split_num,ll length)  //将length分解成split_num部分后的能量
{
    ll each = length / split_num;
    ll pl_1_num = length - each * split_num;
    return (each*each)*(split_num - pl_1_num) + (each + 1)*(each + 1)*pl_1_num;
}


bool judge(ll b[],int n,ll m,ll C)
{
    //将每一段都有f(k)-f(k-1)>=C时,是否能满足所使用的能量总和小于等于m
    ll sum = 0;               
    for(int  i = 0;i<n;i++)  //对每段二分查找最后一个>=C的k值
    {
        if(b[i] == 1) {sum += b[i]*b[i];continue;};  //不可分隔
        ll l = 1,r = b[i]; //加入的点的个数
        //找第一个小于C的
        while(l < r)
        {
            ll mid = (l + r) >> 1;
            if(eval(mid,b[i])- eval(mid+1,b[i]) >= C) l = mid + 1;  //加入l个点分成l+1段
            else r = mid;
        }
        //l--;  //指向最后一个大于等于C的
        sum += eval(l,b[i]);
    }
    return sum <= m;
}



int main()
{
    int n;
    cin >> n;
    ll a[n+1];
    a[0]=0;
    for(int  i =1;i<n+1;i++) cin>>a[i];
    ll b[n];
    for(int i = 1;i<n+1;i++)
    {
        b[i-1] = a[i] - a[i-1];
    }
    ll m;
    cin >> m;
    //外层搜索C   
    ll l = 0,r = 1e18+10;
    while(l<r)
    {
        ll mid = (l+r)>>1;
        if(judge(b,n,m,mid)) l = mid+1;
        else r = mid;
    }   //l=r指向第一个不能满足条件的C
    
    ll sum = 0;
    ll r1 = --r;  //最后一个满足条件的C
    ll ans = 0;
    for(int i = 0;i<n;i++)
    {
        if(b[i] == 1) {sum += b[i]*b[i];continue;};  //不可分隔
        ll l = 1,r = b[i]; //加入的点的个数
        //找第一个小于C的
        while(l < r)
        {
            ll mid = (l + r) >> 1;
            if(eval(mid,b[i])-eval(mid+1,b[i]) >= r1) l = mid + 1;  //加入l个点分成l+1段
            else r = mid;
        }
        //l--;  //指向最后一个大于等于C的
        sum += eval(l,b[i]);
        ans += (l  - 1);
    }
    cout << ans - (m - sum)/r1 << endl;  //需要根据m **去除一些严格等于C的元素**,
    system("pause");
    return 0;
}


  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值