Codeforces Round #766 (Div. 2) [A~F]题解

本文提供了Codeforces Round #766 (Div.2) A~F题目的详细解题思路与代码实现,涵盖了网格染色、座位选择、树边赋值等问题。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

Codeforces Round #766 (Div. 2) [A~F]

A.Not Shading

A题

题意 : 给出一个 n n nx m m m的网格,中间有黑色块和白色块,再给出一个坐标 ( r , c ) (r,c) (r,c),问把 ( r , c ) (r,c) (r,c)处的格子染成黑色最少需要多少次操作。一次操作可以选择一块黑色格子,将该格子所处的一列都变黑,或者将该格子所处的一行都变黑。

思路:次数可能是0次,1次,2次,或者-1(不可能的话输出-1),当 ( r , c ) (r,c) (r,c)本来就是黑色时需要操作0次,当 ( r , c ) (r,c) (r,c)所在行或列存在一个或以上黑格子时,只需要操作1次,否则如果存在至少一个黑色格子,需要操作2次(把一整列变黑然后再把 ( r , c ) (r,c) (r,c)行变黑),如果不存在黑色格子,则不管怎么做都不可能将 ( r , c ) (r,c) (r,c)变黑,输出-1。

#include<bits/stdc++.h>
using namespace std ;
#define ll long long
const ll N = 3e3+9 ;
string s[ N ] ;
int main(){
    ll t ; cin >> t ;
    while( t-- ){
        ll n , m , x , y , b = 0 ; // b 表示是否出现过黑色格子
        cin >> n >> m >> x >> y ;
        for( int i = 0 ; i < n ; i ++ ) cin >> s[ i ] ;

        for( int i = 0 ; i < n ; i ++ )
            for( int j = 0 ; j < m ; j ++ )
                if( s[ i ][ j ] == 'B' ) b = 1 ;

        if( !b ){ // 不存在黑色格子,则不可能将(x,y)染黑
            cout << "-1\n" ; continue ;
        }
        if( s[x-1][y-1] == 'B' ){ // (x,y)本身是黑色
            cout << "0\n" ; continue ;
        }
        else{
            ll f = 0 ; // 检查(x,y)所在行或列是否存在黑色格子
            for( int i = 0 ; i < m ; i ++ )
                if( s[ x - 1 ][ i ] == 'B' ) f = 1 ;
            for( int i = 0 ; i < n ; i ++ )
                if( s[ i ][ y - 1 ] == 'B' ) f = 1 ;
            if( f )
                cout << "1\n" ;
            else
                cout << "2\n" ;
        }
    }
return 0 ;
}

B.Not Sitting

B题

题意 : 小 R R R和小 T T T在一个 N N Nx M M M大小的教室里要选一个座位坐,小 T T T想要坐得离小 R R R尽可能远,但是小 R R R想要离小 T T T尽可能近。小 T T T可以将教室里的某些座位涂上粉红色,这样小 R R R就没法选这些座位,小 R R R先选座,小 T T T再选座。现在问小 T T T在可以将 x x x个座位涂上粉红色时, T T T R R R之间最小的最大距离。 x x x等于 ( 0 , 1 , 2 , 3... n ∗ m − 1 ) (0,1,2,3...n*m-1) (01,2,3...nm1),每一个 x x x都要输出,一共输出 n ∗ m n*m nm次。
思路 : 正着做从小到大考虑 x x x比较难,要分析每加一块粉色涂在哪里最优。但是我们并不需要知道粉色涂在哪里,反过来想,小 T T T每次选座一定选的是教室的四个角之一,这样小 T T T在小 R R R坐下以后离小 R R R尽可能地远,所以小 T T T每次涂成粉色的一定是当前离四个角落最近的那个座位,当可以涂掉 x x x个座位时,一定先涂掉距离最小的前 x x x个座位,输出的答案就是第 x + 1 x+1 x+1小的那个距离。所以我们先把所有格子离角落的最大距离算出来,然后将距离从小到大排序,输出前 n ∗ m n*m nm个即可。

#include<bits/stdc++.h>
using namespace std ;
#define ll long long
const ll N = 2e5+9 ;
ll Max( ll a , ll b ){ return a>b?a:b ; } // 返回最大值
ll Abs( ll x ){ return x<0?-x:x ; } // 返回绝对值
ll a[ N ] , n , m ;
ll maxdis( ll x , ll y ){
    // 找到(i,j)离四个角最大的距离
    ll ans = Abs( x - 1 ) + Abs( y - 1 ) ;
    ans = Max( ans , Abs( x - 1 ) + Abs( m - y ) ) ;
    ans = Max( ans , Abs( n - x ) + Abs( y - 1 ) ) ;
    ans = Max( ans , Abs( n - x ) + Abs( m - y ) ) ;
    return ans ;
}
int main(){
    ll t ; cin >> t ;
    while( t-- ){
        ll cnt = 0 ; cin >> n >> m ;
        //把所有距离计算出来 ,cnt是距离的个数
        for( ll i = 1 ; i <= n ; i ++ )
            for( ll j = 1 ; j <= m ; j ++ )
                a[ ++cnt ] = maxdis( i , j ) ;
        sort( a+1 , a+1+cnt ) ;
        for( int i = 1 ; i <= n*m ; i ++ ) cout << a[ i ] << " " ;
        cout << "\n" ;
    }
return 0 ;
}

C. Not Assigning

C题

题意:给一棵树,要求将每一条边填上边权,使得每相邻的两条边之和是素数,任何一条单独的边权值也是素数。可能的话按照给出边的顺序输出边权,不可能的话输出-1。素数可以重复使用(因为题目没明确说不给)。
思路:这棵树只有是一条链时才有构造的方案,2,3交替使用即可。
证明:任何一个非2的素数都是奇数,可以表示成{偶数+1},那么两个素数相加就是{偶数+偶数+2},加起来还是偶数,偶数一定不是素数,不符合题意,所以这两个素数中一定有一个是2,和就是{偶数+1+2},而如果一棵树它不是一条链,也就是度数大于2时,假设度数等于3,也就是要求三个数,两两相加都是素数,其中我们已经确定一个数是2,一个数是素数了,再加一个非2素数的话,它和另一个素数和会是偶数,不可行,若加一个2的话,它和2的和会是偶数,也不可行,所以无解。得证这棵树只有是一条链时才有构造的方案。

#include<bits/stdc++.h>
using namespace std ;
#define ll long long
const ll N = 2e5+9 ;
ll Max( ll a , ll b ){ return a>b?a:b ; } // 返回最大值
ll Abs( ll x ){ return x<0?-x:x ; } // 返回绝对值
struct node{
    ll t , nxt , i , val ; // 存边,
    // t : 这条边去到的点
    // nxt : 这条边的下一条边(链式前向星存边)
    // i : 边的序号
    // val : 边权
}e[ N ];
ll h[ N ] , ans[ N ] , cnt ;
void dfs( ll x , ll fa , ll f ){
    for( int i = h[ x ] ; i != 0 ; i = e[ i ].nxt ){
        ll y = e[ i ].t ;
        if( y == fa ) continue ;
        // ans 保存答案
        // y结点填不一样的数字
        ans[ e[i].i ] = !f ;
        dfs( y , x , !f ) ;
    }
}
int main(){
    ios::sync_with_stdio(false);
    ll t ; cin >> t ;
    while( t-- ){
        ll n , f = 0 ; cin >> n ;
        memset( h , 0 , sizeof h ) ;
        memset( ans , 0 , sizeof ans ) ; // 初始化
        map<ll,ll>d ; cnt = 0 ; // d :每个点的度数 , cnt : 边的数量
        for( int i = 1 ; i < n ; i ++ ){
            ll x , y ;
            cin >> x >> y ;
            // 加双向边
            e[ ++cnt ].t = y ; e[ cnt ].nxt = h[ x ] ; h[ x ] = cnt ; e[ cnt ].i = i ;
            e[ ++cnt ].t = x ; e[ cnt ].nxt = h[ y ] ; h[ y ] = cnt ; e[ cnt ].i = i ;
            // 结点度数增加
            d[ x ] ++ ; d[ y ] ++ ;
            // f 表示是否有结点度数超过3,若有结点度数超过3则不是一条链
            if( f || d[ x ] >= 3 || d[ y ] >= 3 ) f = 1 ;
        }
        // 不是一条链则无解
        if( f ){ cout << "-1\n" ; continue ; }

        // 找到一个度数为1的结点做树根,遍历整条链
        for( ll i = 1 ; i <= n ; i ++ ) // dfs( 当前结点 , 当前结点的父节点 , 选数状态(1填3,0填2) )
        if( d[ i ] == 1 ){ dfs( i , -1 , 1 ) ; break ; }

        for( int i = 1 ; i < n ; i ++ )
            if( ans[i] ) cout << "3 " ;
        else cout << "2 " ;
        cout << "\n" ;
    }
return 0 ;
}

上面的代码用G++14跑过了,20却不行
可能是存图写复杂了,建议换种更简便的写法<(^-^)>
怪事情

D. Not Adding

题意 : 给一个序列,每个数都不相同,我们可以进行一种操作,每次操作选择序列中的两个数字,如果它们的gcd(最大公约数)不在序列中,则将gcd加入这个序列,它也在后续操作的选择范围内,问最后我们最多可以对这个序列加多少个数字。
思路 : 从1到n检查数字是否会出现,如果x会出现的话,一定是有两个数的gcd等于x , 这两个数一定是x的倍数,但是因为不知道是哪两个,所以我们直接检查所有x的倍数,看它们的gcd是否等于x即可。因为是gcd,所以就算会有很大的倍数,只要x出现过,它们的gcd也会等于x。也因为是从小到大检查x的,所以小于x的数都可以不管(不过第二层for已经从x开始了,小于x当然不关它的事啊喂x)。然后统计加了多少个数字即可。

#include<bits/stdc++.h>
using namespace std ;
#define ll long long
const ll N = 1e6+9 ;
ll Max( ll a , ll b ){ return a>b?a:b ; } // 返回最大值
ll Abs( ll x ){ return x<0?-x:x ; } // 返回绝对值
ll gcd(ll a,ll b){return b==0?a:gcd(b,a%b);} // 求gcd
ll vis[ N ] ;
int main(){
    ll n , ans = 0 , x ; cin >> n ;
    for( int i = 1 ; i <= n ; i ++ ){
        cin >> x ; vis[ x ] = 1 ; // vis[ x ] : 表示x是否出现过
    }
    for( ll i = 1 ; i <= 1000000 ; i ++ ){
        ll g = 0 ;
        for( ll j = i ; j <= 1000000 ; j += i )
            if( vis[ j ] ) g = gcd( g , j ) ; // 求得i所有倍数的gcd

        if( g == i && !vis[ i ] ) ans ++ ;
    }
    cout << ans << "\n" ;
return 0 ;
}

E. Not Escaping

E题

题意:给出一个NxM的网格,一个人要从(1,1)逃生到(N,M),它可以在同一层中移动,第 i i i层有一个参数 a i a_i ai,移动损耗 ( a i ∗ 移 动 距 离 ) (a_i*移动距离) (ai)的生命值,网格中有一些可能重复的逃生梯,梯子只能从下往上走,第 i i i个梯子可以从 ( a i , b i ) (a_i,b_i) (ai,bi)点移动到 ( c i , d i ) (c_i,d_i) (ci,di)点,并且回复 h i h_i hi的生命值。问这个人逃生成功的最小生命损耗值是多少(生命值负数也算逃生成功)。
思路:每一个梯子的起点和终点称为关键点,我们可以从下往上一层一层地更新到达这一层的关键点需要的最小损耗,然后得到到达( n , m )点的最小损耗 。

(解法来自于wls : wls的B站讲解传送门 E题在25min )

#include<bits/stdc++.h>
using namespace std ;
#define ll long long
#define aa first
#define bb second
const ll N = 1e6+9 ;
ll Max( ll a , ll b ){ return a>b?a:b ; } // 返回最大值
ll Min( ll a , ll b ){ return a<b?a:b ; } // 返回最小值
ll Abs( ll x ){ return x<0?-x:x ; } // 返回绝对值
ll gcd(ll a,ll b){return b==0?a:gcd(b,a%b);} // 求gcd
map< pair<ll,ll> , ll > idx ; // idx[逃生梯起点] = 关键点编号
vector< pair<ll,ll> > r[ N ] ; // r[ 行 ] :{关键点坐标,编号} .......
ll T , n , m , q , a[ N ] , cnt ;
ll f[ N ] ; // f[ 关键点编号 ] = 到达关键点时的最小损耗值
vector< pair<ll,ll> > w[ N ] ; // w[ 关键点 ] :{ 到达点编号 , 高度 } .......
int main(){
    cin >> T ;
    while( T-- ){
        cin >> n >> m >> q ; // 行,列,逃生梯数
        for( int i = 1 ; i <= n ; i ++ )
            cin >> a[ i ] , r[ i ].clear() ; // 层i特性
        cnt = 0 ; idx.clear() ;
        for( int i = 1 ; i <= q ; i ++ ){
            ll a , b , c , d , h ;
            cin >> a >> b >> c >> d >> h ;
            pair<ll,ll> s = make_pair( a , b ) , t = make_pair( c , d ) ;
            if( idx.find(s) == idx.end() ) // s在map里没出现过
                idx[ s ] = ++cnt , r[ a ].push_back( make_pair(b,cnt) ) ;
            if( idx.find(t) == idx.end() ) // t在map里没出现过
                idx[ t ] = ++cnt , r[ c ].push_back( make_pair(d,cnt) ) ;
            // cnt是关键点的个数
            // idx[逃生梯起点] = 关键点编号
            w[ idx[s] ].push_back( make_pair(idx[t],h) ) ;
            // w[ 关键点 ] :{ 到达点编号 , 高度 } .......
            // r[ 行 ] :{关键点坐标,编号} .......
        }
        // f[ 关键点编号 ] = 到达关键点时的最小损耗值
        for( int i = 1 ; i <= cnt ; i ++ )
            f[ i ] = 1ll << 60ll ; // 将所有关键点初始化为不可到达状态
            //初始化为不可到达状态
        for( auto i : r[1] )
            f[i.bb] = 1ll*(i.aa-1)*a[1] ;
            // 第1行中从(1,1)移动到编号为bb的关键点时损耗的生命值
        for( int i = 1 ; i <= n ; i ++ )
            // 逐行更新
            if( r[ i ].size() ){// 如果这一行存在关键点
                ll m = r[ i ].size() ;
                sort( r[i].begin() , r[i].end() ) ; // 关键点按照横坐标排序
                ll M = 1ll << 61ll ; // 初始话M为极大值
                // 从左往右更新一下
                for( int j = 0 ; j < m ; j ++ ){
                    // M : 表示表达式中最大损耗(M是负数)
                    // 在第i层从(i,x)移动到(i,y) x<y
                    // 表达式 : 损耗 = 到达y的损耗+(y-a)*ai = 到达y的损耗 + y*ai - x*ai ,在这里M表示-x*ai 
                    M = Min( M , f[ r[i][j].bb ] - 1ll*r[ i ][ j ].aa*a[ i ] ) ;
                    f[ r[i][j].bb ] = Min( f[ r[i][j].bb ] , M + 1ll*r[i][j].aa*a[i] ) ;
                }
                // 从右往左更新一下
                M = 1ll << 61ll ;
                for( int j = m - 1 ; j >= 0 ; j -- ){
                    // M : 表示表达式中最小损耗(M是正数)
                    // 在第i层从(i,y)移动到(i,x) x<y
                    // 表达式 : 损耗 = 到达y的损耗+(y-x)*ai = 到达y的损耗+y*ai - a*ai ,在这里M表示到达y的损耗+y*ai 
                    M = Min( M , f[ r[i][j].bb ] + 1ll*r[i][j].aa*a[i] ) ;
                    f[ r[i][j].bb ] = Min( f[ r[i][j].bb ] , M-1ll*r[i][j].aa*a[i] ) ;
                }
                // 更新下一层
                for( int j = 0 ; j < m ; j ++ ){
                    // 如果编号为r[i][j].bb的关键点可以到达
                    if( f[ r[i][j].bb ] < 1ll << 60ll ) // w[ 关键点编号 ] :{ 到达点编号 , 恢复的生命值 } .......
                        for( auto k : w[ r[i][j].bb ] ) // k : 到达点的编号
                            f[ k.aa ] = Min( f[k.aa] , f[ r[i][j].bb ] - k.bb ) ;
                            //  min去掉是不是也是对的?
                            //  不对!因为其它梯子也可能到达这个点
                }
            }
        ll ans = 1ll << 60ll ;
        for( auto i : r[n] ) // 单独更新第n层
            ans = Min( ans , f[i.bb] + 1ll*(m-i.aa)*a[n] ) ;
        if( ans < 1ll << 60ll )
            cout << ans << "\n" ;
        else cout << "NO ESCAPE\n" ;
        for( int i = 1 ; i <= cnt ; i ++ ) w[ i ].clear() ;
    }
}

F. Not Splitting

F题

题意 :给n对连接的方块,问如何将k*k的矩阵划为全等的两部分,让破坏的链接块尽可能的小。输出保留下来的连接块的对数。(链接块都是两个黏在一起的小正方形)
思路 : 切割线必须是旋转 180 ° 180° 180°对称的(因为两个图形全等)(在官方题解处有几何证明)。将网格看成一张图,要最小化破坏的连接块的数量,等于使切割路线尽可能地少经过连接块共享的那条边,我们把共享边权值看作是1,其它边权看作是0,由于每个切口都是围绕中心旋转对称的,我们可以考虑寻找一条从边界到中心的权值最小的路径,然后旋转这条路径180°来得到整条有效的切割线。但旋转后的路径切割的方块数量不一定是最优的,我们可以把另一半存在的共享边,旋转180°到这一半来,也就是把权值加到这一部分来,再寻找前一半的最短路径,得到整体的最优切割线。

解法来自官方题解 :官方题解传送门

#include<bits/stdc++.h>
using namespace std ;
#define ll long long
#define aa first
#define bb second
const ll N = 1e6+9 ;
ll Max( ll a , ll b ){ return a>b?a:b ; } // 返回最大值
ll Min( ll a , ll b ){ return a<b?a:b ; } // 返回最小值
ll Abs( ll x ){ return x<0?-x:x ; } // 返回绝对值
ll gcd(ll a,ll b){return b==0?a:gcd(b,a%b);} // 求gcd
void swapp( ll &a , ll &b ){ ll t = a ; a = b ; b = t ; } // 交换两个数
ll n , k ;
ll h[ 600 ][ 600 ] , v[ 600 ][ 600 ] , dis[ 600 ][ 600 ] ; // h : 竖边的权重 , v : 横边的权重
struct node{
    ll x , y , w ;
    friend bool operator<(const node&a,const node&b){
        return a.w>b.w;//sort时<小的在前 优先队列时>小的在前
    }
};
int main(){
    ll t ; cin >> t ;
    while( t-- ){
        memset( dis , 127 , sizeof dis ) ; // 初始化每个点都为不可到达状态
        memset( h , 0 , sizeof h ) ;
        memset( v , 0 , sizeof v ) ;
        cin >> n >> k ;
        for( int i = 1 ; i <= n ; i ++ ){
            ll x1 , y1 , x2 , y2 ;
            cin >> x1 >> y1 >> x2 >> y2 ;
            //退格成点,起点是(0,0)
            x1 -- ; y1 -- ; x2 -- ; y2 -- ;
            if( x1 == x2 ){ // 竖边
                if( y1 > y2 ) swapp( y1 , y2 ) ;
                v[ x1 ][ y2 ] ++ ;
                v[ k - x1 - 1 ][ k - y2 ] ++ ; // 旋转180°后的权重也加进去
            }
            else{ // 横边
                if( x1 > x2 ) swapp( x1 , x2 ) ;
                h[ x2 ][ y1 ] ++ ;
                h[ k - x2 ][ k - y1 - 1 ] ++ ;
            }
        }
        //迪杰斯特拉找最短路
        priority_queue<node>q;
        q.push({0,0,0}) ;
        dis[0][0] = 0 ;
        while( !q.empty() ){
            ll x , y , w ; x = q.top().x ; y = q.top().y ; w = q.top().w ;
            q.pop() ;
            if( w > dis[ x ][ y ] ) continue ;
            // 从一个点出发上下左右走
            // 竖边的边权存在竖边上端点处
            // 横边的边权存在横边左端点处
            // 向上走
            if( x > 0 && w + v[ x - 1 ][ y ] < dis[ x - 1 ][ y ] ){
                dis[ x - 1 ][ y ] = w + v[ x - 1 ][ y ] ;
                q.push( { x-1 , y , dis[x-1][y] } ) ;
            }
            // 向下走
            if( x < k && w + v[ x ][ y ] < dis[ x + 1 ][ y ] ){
                dis[ x + 1 ][ y ] = w + v[ x ][ y ] ;
                q.push( { x+1 , y , dis[x+1][y] } ) ;
            }
            // 向左走
            if( y > 0 && w + h[ x ][ y - 1 ] < dis[ x ][ y - 1 ] ){
                dis[ x ][ y - 1 ] = w + h[ x ][ y - 1 ] ;
                q.push( { x , y - 1 , dis[ x ][ y - 1 ] } ) ;
            }
            // 向右走
            if( y < k && w + h[ x ][ y ] < dis[ x ][ y + 1 ] ){
                dis[ x ][ y + 1 ] = w + h[ x ][ y ] ;
                q.push( { x , y + 1 , dis[ x ][ y + 1 ] } ) ;
            }
        }
        cout << n - dis[ k/2 ][ k/2 ] << "\n" ;
    }
return 0 ;

}

如上述有误,欢迎各位批评指正,不胜感激0v0

评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值