Div 4 7.23 模拟赛题解

\(\textup{Div 4 7.23}\) 模拟赛题解

作者:wendywan

\(\textup{T0}\) 写在前面

不是甩锅。

T6 应该有人训练做过,可是老师不给撤。

我想组部分分的,可是老师懒得搞。

本来不想放模拟的,可是老师说思维太多了所以就放个小模拟。

最后是既练习了模拟 思维 暴力 计数和 dp,还只有一点难度难道不好吗。

预期大众分是 \([350, 400]\),123应该是人均题,差太多欢迎来骂我。

原来那套题的前言

说真的,我觉得我出的都是大水题。

黄是下位黄,绿是下位绿。

出一些想一想就可以做出来的题目,这样大家都会很开心的对身体好。

出一些听一听就可以改出来的题目,这样大家都会很快乐的对心理好。

最后是既练习了图论思维数据结构树和 dp,还一点难度都没有难道不好吗。


\(\textup{T1}\) VJEKO

\(\textup{Link.}\)

模拟题。

考虑到星号有且只有一个,那么我们只需要考虑:

  • 前缀加后缀大于文件名;
  • 开头结尾匹配。

这样就做完了,具体实现可以看代码。

$\textup{Code.}$
#include<bits/stdc++.h>
#define int long long
using namespace std;
int T, k;
int star = -1;
string s1, s2;
void slove(){
    cin >> s2;
    int n = s2.size();
    s2 = ' ' + s2;
    bool flag = 0;
    int pre = star - 1, aft = k - star, cnt = 0, lst = -1;
    if( n < pre + aft - 1 ){//长度太小
        cout << "NE\n";
        return;
    }

    for( int i = 1; i <= pre; i ++ ){
        if( s1[i] != s2[i] ){//匹配前缀
            cout << "NE\n";
            flag = 1;
            break;
        }
        cnt ++;
    }
    if( flag ) return;
    for( int i = 1; i <= n; i ++ ){//匹配后缀
        if( s1[cnt + 1] == s2[i] ) cnt ++;//匹配到了
        else{//如果不匹配
            if( s1[cnt + 1] == '*' ){//遇到*,跨过*
                lst = ++ cnt;
                continue;
            }
            if( lst == -1 ){//没有碰到*
                cout << "NE\n";
                flag = 1;
                break;
            }
            else{//之前碰到过星号,但是这段配不上,就一整段回溯算作星号
                cnt = lst;
                if( s1[cnt + 1] == s2[i] ) cnt ++;
            }
        }
    }
    if( flag ) return;
    else if( cnt != k ) cout << "NE\n";
    else cout << "DA\n";
}
signed main(){
    // freopen( "vjeko.in", "r", stdin );
    // freopen( "vjeko.out", "w", stdout );
    cin >> T >> s1;
    k = s1.size();
    s1 = ' ' + s1;
    for( int i = 1; i <= k; i ++ ){//找到星号的位置
        if( s1[i] == '*' ){
            star = i;
            break;
        }
    }
    while( T -- )
        slove();
    return 0;
}

\(\textup{T2}\) Number of Pairs

\(\textup{Link.}\)

数据范围很小,排序。

暴力枚举每个数,二分出满足条件的左节点和右节点。

统计,\(O(N \cdot \log N)\) 做完了。

$\textup{Code 1.}$

手写二分,有点丑。

#include<bits/stdc++.h>
#define int long long
using namespace std;
const int MAXN = 2e5 + 5;
int T;
int N, l, r;
int a[MAXN];
bool chk1( int i, int x ){
    return a[i] + a[x] >= l;
}
bool chk2( int i, int x ){
    return a[i] + a[x] <= r;
}
void solve(){
    cin >> N >> l >> r;
    for( int i = 1; i <= N; i ++ ){
        cin >> a[i];
    }
    sort( a + 1, a + N + 1 );
    int ans = 0;
    for( int i = 1; i < N; i ++ ){
        int lft = N + 1, rgt = i, L = i + 1, R = min( MAXN, N );
        while( L <= R ){
            int mid = R - ( ( R - L ) >> 1 );
            if( chk1( i, mid ) ) R = mid - 1, lft = mid;
            else L = mid + 1;
        }
        L = lft, R = min( MAXN, N );
        while( L <= R ){
            int mid = R - ( ( R - L ) >> 1 );
            if( chk2( i, mid ) ) L = mid + 1, rgt = mid;
            else R = mid - 1;
        }
        // cerr << lft << " " << rgt << endl;
        ans += max( 0LL, rgt - lft + 1 );
    }
    cout << ans << endl;
}

signed main(){
    cin >> T;
    while( T -- )
        solve();
    return 0;
}
$\textup{Code 2.}$

用 STL。

#include<bits/stdc++.h>
#define int long long
using namespace std;
const int MAXN = 2e5 + 5;
int T;
int x, N, l, r, ans;
vector<int> a;
void solve(){
    ans = 0;
    cin >> N >> l >> r;
    a.clear();
    for( int i = 1; i <= N; i ++ ){
       cin >> x;
       a.push_back( x ); 
    }
    sort( a.begin(), a.end() );
    for( int i = 0; i < N; i ++ ){
        auto it1 = lower_bound( a.begin() + i + 1, a.end(), l - a[i] );
        auto it2 = upper_bound( a.begin() + i + 1, a.end(), r - a[i] );
        ans += ( it2 - it1 );
    }
    cout << ans << endl;
}

signed main(){
    cin >> T;
    while( T -- )
        solve();
    return 0;
}

\(\textup{T3}\) Riko

\(\textup{Link.}\)

思维题,做的时候还想了一会儿,没场掉去做 D 了。

可见我非常的菜。

至于为什么没切……你不会和我一样没注意到,题目规定了每根棍子都只能分成两个吧。

那么就只有两种情况了。

考虑第一种情况,所有的棍子都断成两截,如果 \(N\) 不是偶数就不存在。

直接排序首尾配对,判一下合不合法就行。

若有若干个没有断成两个的,那么一定有 \(L = a_{N \cdots}\),扫一下记录一下就行。

这么一想还是很简单的吧。

$\textup{Code.}$

我代码里面两种顺序是反过来的别看错了。

#include<bits/stdc++.h>

using namespace std;
const int MAXN = 3e5 + 5;
int N;
int a[MAXN];
int maxx, minn = 0x3f3f3f3f;
int b[MAXN], tot;
bool chk1(){
    sort( b + 1, b + tot + 1 );
    if( tot % 2 ) return false;
    for( int i = 1; i <= tot / 2; i ++ ){
        if( ( b[i] + b[tot - i + 1] ) != maxx )
            return false;
    }
    return true;
}
bool chk2(){
    sort( a + 1, a + N + 1 );
    if( N % 2 ) return false;
    for( int i = 1; i <= N / 2; i ++ ){
        if( ( a[i] + a[N - i + 1] ) != ( maxx + minn ) )
            return false;
    }
    return true;
}
int main(){
    cin >> N;
    for( int i = 1; i <= N; i ++ ){
        cin >> a[i];
        maxx = max( maxx, a[i] );
        minn = min( minn, a[i] );
    }
    
    for( int i = 1; i <= N; i ++ ){
        if( a[i] != maxx ) b[++ tot] = a[i];
    }
    if( chk1() ) cout << maxx << " ";
    if( chk2() ) cout << minn + maxx;
    return 0;
}

\(\textup{T4}\) 相聚

\(\textup{Link.}\)

随机抽到的题,和几个朋友瞪了十几分钟才想出 dp 还有救嘛。

在此之前思考到一些贪心的性质,比如先排一遍序会更好操作,比如三只以上的海狸一起移到一个点一定是不优的。

证明是容易的,因为四只就可以两两配对一定不会更劣。

这个时候再去想 dp 就稍易一点了,设 \(f_i\) 表示前 \(i\) 只相聚后的最小代价。

那么对于每只海狸,只有两种方式抱团,那么也就对应着两种转移:

\[f_i = \min \begin{cases} f_{i - 2} + a_i - a_{i - 1} \\ f_{i - 3} + a_i - a_{i - 2} \end{cases} \]

$\textup{Code.}$
#include<bits/stdc++.h>

using namespace std;
const int inf = 0x3f3f3f3f;
const int MAXN = 3e5 + 5;
int N;
int x[MAXN], sum, dp[MAXN];
int ans = 0;
int main(){
    cin >> N;
    for( int i = 1; i <= N; i ++ )
        cin >> x[i];
    
    sort( x + 1, x + N + 1 );
    dp[0] = 0;
    dp[1] = inf;
    dp[2] = x[2] - x[1], dp[3] = x[3] - x[1];
    for( int i = 4; i <= N; i ++ ){
        dp[i] = min( dp[i - 3] + x[i] - x[i - 2], dp[i - 2] + x[i] - x[i - 1] );
        // cerr << dp[i] << " ";
    }
    cout << dp[N];
    return 0;
}

\(\textup{T5}\) Vlad and Avoiding X

\(\textup{Link.}\)

暴搜 \(O(2^{49})\),考虑优化。

乱搞做法。

如图,happy12313大师发现棋盘中一圈都不可以扫到。

于是我们只用扫剩下 \(21\) 个点,然后又记忆化,于是冲过去了。

复杂度是抽象的。

发现影响黑格的格子是对角上的,于是先将整个棋盘间隔黑白染色,互相影响的格子一定是同色的。

染色可能没说清楚,就是常规的黑白染色。

于是放个图。

不会影响,于是直接分治做。

即分成两类,搜索使得每一类都没有非法的格子。

再给个优化,发现答案最大就是操作中间八个格子。

为什么呢?

如下图,绿色勾子表示我确保这个点非黑,而橙色点点是它控制的范围。

即橙色点位置即使是黑色也不会非法了。

而就像前面乱搞做法所说,灰色点不管怎么变换也不会非法。

即一类四到五个,另一类同理。

在分治的时候判一下优化即可。

$\textup{Code.}$
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int MAXN = 10;
int N = 7, T;
bool mp[MAXN][MAXN];
bool chk( bool mp[][MAXN], int col ){//判断整个棋盘是否合法
    for( int i = 2; i < N; i ++ ){
        for( int j = 2; j < N; j ++ ){
            if( ( i + j ) % 2 != col ) continue;
            if( mp[i][j] && mp[i + 1][j + 1] && mp[i - 1][j - 1] && mp[i + 1][j - 1] && mp[i - 1][j + 1] )
                return false;
        }
    }
    return true;
}
int ans = 8;
bool dfs( bool a[][MAXN], vector<pair<int, int>> & grp, int idx, int nowans, int col ){
    //idx表示当前正在考虑grp[idx]这个点(dfs深度) nowans表示还剩几步 col表示当前是第几类
    if( nowans < 0 ) return false;//如果没次数了
    if( grp.size() - idx < nowans ) return false;
    if( idx == grp.size() ) return chk( a, col );
    if( dfs( a, grp, idx + 1, nowans, col ) ) return true;//根本就不用改这个点也能合法
    int x = grp[idx].first, y = grp[idx].second;
    a[x][y] = !a[x][y];
    if( dfs( a, grp, idx + 1, nowans - 1, col ) ){
        a[x][y] = !a[x][y];//这步和后面第30行都是回溯
        return true;  
    }
    a[x][y] = !a[x][y];
    return false;  
}
signed main(){
    cin >> T;
    while( T -- ){
        for( int i = 1; i <= N; i ++ ){
            for( int j = 1; j <= N; j ++ ){
                char ch;
                cin >> ch;
                mp[i][j] = ( ch == 'B' );
            }
        }
        vector<pair<int, int>> grp[3];//存储两个类
        for( int i = 1; i <= N; i ++ ){
            for( int j = 1; j <= N; j ++ ){
                grp[( i + j ) % 2].push_back({ i, j });
            }
        }
        int ans = 0;
        for( int col = 0; col <= 1; col ++ ){//棋盘染色后的两种
            int cnt = 5;
            for( int i = 0; i <= 4; i ++ ){//枚举最小操作次数
                if( dfs( mp, grp[col], 0, i, col ) ){
                    cnt = i;
                    break;
                }
            }
            ans += cnt;
        }
        cout << ans << endl;
    }
    return 0;
}

\(\textup{T6}\) ∙ (Bullet)

\(\textup{Link.}\)

首先,看见奇怪的乘积柿子可以试图移项进行一些操作。

我们将原式变为,并称这个为每条鱼的价值:

\[\frac{A_i}{B_i} = -\frac{B_j}{A_j} \]

这说明什么?

当一条鱼的 \(A\) 值和 \(B\) 值相除得到的价值与另一条鱼的价值取反再取倒数相等,它们就不能被放在一起。

接下来,我们考虑如何统计答案。

我们先不考虑非法情况,那么总情况数就是 \(2^n\) 对吧。

为什么呢?

因为每种鱼都有选和不选两种情况。那如果将这个想法放在非法和合法混在一起的方案中呢?

那原来相乘的一坨鱼,就不能放在一起乘了,得分开然后相加。

那这样实现就非常容易了。

注意,有可能会有多条鱼约分后价值相同,为了方便于是就一起统计。

举个例子

没看懂上面那一大段文字可以直接看这里。

我们设有两种鱼 \(x\)\(y\) 分别有 \(c\)\(d\) 条,且这两条鱼的价值(定义如上)正好就是相反数取倒数的非法关系。

那么显然当前贡献就是:

\[2^c \times ans_{lst} + 2^d \times ans_{lst} = (2^c+2^d) \times ans_{lst} \]

吗?

其实还要减一,因为两边都是空集的情况被计算两次。

最终式子是这样的:

\[(2^c+2^d - 1) \times ans_{lst} \]

最后,讲一下实现。

开一个 \(map\) 存每条鱼的价值,记得存最简分数形式,并且自己定好负号等细节应该在哪里标记。

注意要特判一下 \(x\)\(y\) 全为 \(0\) 的情况,且最后要减掉全空的情况。

然后就维护一个 \(ans\) 不停累乘就可以了。

$\textup{Code.}$
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int MAXN = 2e5 + 5;
const int MOD = 1000000007;
int N, ans = 1, cnt;
int pow2[MAXN];
struct node{
    int x, y;
    bool operator < ( const node & b ) const {
		return x == b.x ? y < b.y : x < b.x;
	}
}fi[MAXN];

map<node, int> mp;

node pre( int x, int y ){
    int k = __gcd( abs( x ), abs( y ) ) == 0 ? 1 : __gcd( abs( x ), abs( y ) );
    x /= k, y /= k;//化成最简分数
    if( y < 0 || ( x < 0 && y == 0 ) )
        x = -x, y = -y;//统一价值正负号位置
    return { x, y };
}
signed main(){
    pow2[0] = 1;
    for( int i = 1; i < MAXN; i ++ )
        pow2[i] = pow2[i - 1] * 2 % MOD;
    //预处理2的幂:这个真的是必要的,如果直接用位运算会炸掉的!
    cin >> N;
    for( int i = 1; i <= N; i ++ ){
        cin >> fi[i].x >> fi[i].y;
        if( fi[i].x == 0 && fi[i].y == 0 ){
            cnt ++;//统计为0情况
            continue;
        }
        fi[i] = pre( fi[i].x, fi[i].y );
        mp[fi[i]] ++;//加入map
    }
    for( int i = 1; i <= N; i ++ ){//逐种同价值的鱼进行统计答案
        int x = fi[i].x, y = fi[i].y;
        ans = ( ans * ( ( pow2[mp[{ x, y }] + mp[{ -x, -y }]] + pow2[mp[{ y, -x }] + mp[{ -y, x }]] - 1 ) % MOD ) ) % MOD;//统计答案记得减一!
        mp[{ x, y }] = mp[{ -x, -y }] = 0;
        mp[{ y, -x }] = mp[{ -y, x }] = 0;
        //将已经加过的部分去除
    }
    cout << ( ( ans + cnt ) % MOD + MOD - 1 ) % MOD;
    return 0;
}

被删掉的题

\(\textup{T1}\) 货物运输

\(\textup{Link.}\)

假定我们要返回根,此时答案是好求的,即两倍的边权和。

而我们可以不返回根,故贪心地想,去掉最长的那条边折返所带来的代价。

实现上 dfs 扫一下找最长边就做完了。

$\textup{Code.}$
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int MAXN = 1e5 + 5;
int N;
struct star{
    int nxt, to, w;
}edge[MAXN << 1];
int head[MAXN], cnt;
void add( int u, int v, int w ){
    edge[++ cnt] = { head[u], v, w };
    head[u] = cnt;
}
int ans = 0, ccnt = 0;
void dfs( int u, int fa, int now ){
    bool flag = 0;
    for( int i = head[u]; i; i = edge[i].nxt ){
        int v = edge[i].to;
        if( v == fa ) continue;
        dfs( v, u, now + edge[i].w );
        flag = true;
    }
    if( !flag ) ccnt = max( ccnt, now );
}
signed main(){
    cin >> N;
    for( int i = 1; i < N; i ++ ){
        int u, v, w;
        cin >> u >> v >> w;
        add( u, v, w ), add( v, u, w );
        ans += 2 * w;
    }
    dfs( 1, 0, 0 );
    cout << ans - ccnt;
    return 0;
}

\(\textup{T2}\) 气温变化

\(\textup{Link.}\)

比前面那个题还送。

题目要求一个滑动窗口的极差,可以用单调队列线段树之类的数据结构简单维护。

当然遇到这种动态维护最大最小值的个人认为 set 是最简单无脑的呀。

$\textup{Code.}$
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int MAXN = 2e5 + 5;
int N, K;
int h[MAXN], ans;
multiset<int> st;
signed main(){
    cin >> N >> K;
    for( int i = 1; i <= N; i ++ ){
        cin >> h[i];
        if( i < K ) st.insert( h[i] );
    }
    for( int i = K; i <= N; i ++ ){
        st.insert( h[i] );
        auto it1 = st.end();
        it1 --;
        auto it2 = st.begin();
        // cerr << *it1 << " " << *it2 << endl;
        ans = max( ans, *it1 - *it2 );
        st.erase( st.find( h[i - K + 1] ) );
    }
    cout << ans;
    return 0;
}

\(\textup{T4}\) Strange Lunchbox

\(\textup{Link.}\)

这个题的话两年前写的所以懒得拟题解了。

但是我觉得这个题比前面那个暴力题好,尽管前面可以锻炼一下数据结构但是没啥难度。

考虑定义 \(dp[i][j][k]\) 表示扫到第 \(i\) 位,\(a\) 取了 \(j\) 个,\(b\) 取了 \(k\) 个时的最小购买套餐份数。

\(\textup{T4}\) Bandit in a City

\(\textup{Link.}\)

最大最小,一看就很二分。

确实有二分做法,好像是 \(O( N \cdot \log N )\) 做法。

这里给出树形 dp 做法。

状态定义:

其实也很好想到,我们将大问题拆成子问题去解决。

定义 \(dp_u\) 表示以 \(u\) 为根的子树中,其权值最大的叶子的最小的值。

状态转移和初始化:

我们计算出当前子树中儿子权值最大的数,并且贪心的将当前节点的权值进行分配。

由于为了最小化最大权值,我们可以尽量让叶子们得到的权值平均。

从而定义 \(sz_u\) 表示以 \(u\) 为根的子树中,有多少个叶子节点。

\(sum_u\) 表示以 \(u\) 为根的子树中,最终需要分配到其子树叶子节点的总权值。

先处理出:

\[dp_u = \max dp_{to} \]

并预处理 \(sz\)\(sum\)

考虑要尽量贪心地分配,于是有:

\[dp_u = \max\left( dp_u,\ \left\lceil \frac{sum_u}{sz_u} \right\rceil \right) \]

解释一下,就是将所有儿子所需要分配的人又重新平均分配了一遍。

但要与原来已经处理完子树取 \(max\) 是因为万一我这种分配方法无法适用于最大的那个子树,就得满足那个子树的大小咯。

答案显然是 \(dp_1\)

$\textup{Code.}$
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int MAXN = 2e5 + 5;
int N;
int p, a[MAXN];
struct star{
    int nxt, to;
}edge[MAXN * 2];
int head[MAXN], cnt;
void add( int u, int v ){
    edge[++ cnt] = { head[u], v };
    head[u] = cnt;
}
int sum[MAXN];//表示以u为根的子树中,最终需要分配到其子树叶子节点的总权值
int dp[MAXN];//表示在u子树内最后权值最大的叶子节点权值最小的值
int sz[MAXN];//表示u的子树中有多少个是叶子节点
void dfs( int u, int fa ){
    bool flag = true;//记录是否到叶子
    sum[u] += a[u];//初始化自己价值
    for( int i = head[u]; i; i = edge[i].nxt ){
        int to = edge[i].to;
        if( to == fa ) continue;
        flag = false;//找到儿子,即非叶子
        dfs( to, u );
        sz[u] += sz[to], sum[u] += sum[to];//记录大小
        dp[u] = max( dp[u], dp[to] );//找到儿子中最大价值的叶子
    }
    if( flag ){
        sz[u] = 1, dp[u] = a[u], sum[u] = a[u];//如果是叶子,初始化
        return;
    }
    dp[u] = max( dp[u], ( sum[u] + sz[u] - 1 ) / sz[u] );//贪心地分配个每个叶子节点
    //这个式子等价于ceil( 1.0 * sum[u] / sz[u] )
}

signed main(){
    cin >> N;
    for( int i = 2; i <= N; i ++ ){
        cin >> p;
        add( p, i ), add( i, p );
    }
    for( int i = 1; i <= N; i ++ ) cin >> a[i];
    dfs( 1, 0 );
    cout << dp[1];
    return 0;
}
posted @ 2026-07-22 09:48  Fαll  阅读(122)  评论(0)    收藏  举报