Div 4 7.23 模拟赛题解
\(\textup{Div 4 7.23}\) 模拟赛题解
作者:wendywan
\(\textup{T0}\) 写在前面
不是甩锅。
T6 应该有人训练做过,可是老师不给撤。
我想组部分分的,可是老师懒得搞。
本来不想放模拟的,可是老师说思维太多了所以就放个小模拟。
最后是既练习了模拟 思维 暴力 计数和 dp,还只有一点难度难道不好吗。
预期大众分是 \([350, 400]\),123应该是人均题,差太多欢迎来骂我。
原来那套题的前言
说真的,我觉得我出的都是大水题。
黄是下位黄,绿是下位绿。
出一些想一想就可以做出来的题目,这样大家都会很开心的对身体好。
出一些听一听就可以改出来的题目,这样大家都会很快乐的对心理好。
最后是既练习了图论思维数据结构树和 dp,还一点难度都没有难道不好吗。
\(\textup{T1}\) VJEKO
模拟题。
考虑到星号有且只有一个,那么我们只需要考虑:
- 前缀加后缀大于文件名;
- 开头结尾匹配。
这样就做完了,具体实现可以看代码。
$\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
数据范围很小,排序。
暴力枚举每个数,二分出满足条件的左节点和右节点。
统计,\(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
思维题,做的时候还想了一会儿,没场掉去做 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}\) 相聚
随机抽到的题,和几个朋友瞪了十几分钟才想出 dp 还有救嘛。
在此之前思考到一些贪心的性质,比如先排一遍序会更好操作,比如三只以上的海狸一起移到一个点一定是不优的。
证明是容易的,因为四只就可以两两配对一定不会更劣。
这个时候再去想 dp 就稍易一点了,设 \(f_i\) 表示前 \(i\) 只相聚后的最小代价。
那么对于每只海狸,只有两种方式抱团,那么也就对应着两种转移:
$\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
暴搜 \(O(2^{49})\),考虑优化。
发现影响黑格的格子是对角上的,于是先将整个棋盘间隔黑白染色,互相影响的格子一定是同色的。
染色可能没说清楚,就是常规的黑白染色。
于是放个图。

不会影响,于是直接分治做。
即分成两类,搜索使得每一类都没有非法的格子。
再给个优化,发现答案最大就是操作中间八个格子。
为什么呢?
如下图,绿色勾子表示我确保这个点非黑,而橙色点点是它控制的范围。
即橙色点位置即使是黑色也不会非法了。
而就像前面乱搞做法所说,灰色点不管怎么变换也不会非法。

即一类四到五个,另一类同理。
在分治的时候判一下优化即可。
$\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)
首先,看见奇怪的乘积柿子可以试图移项进行一些操作。
我们将原式变为,并称这个为每条鱼的价值:
这说明什么?
当一条鱼的 \(A\) 值和 \(B\) 值相除得到的价值与另一条鱼的价值取反再取倒数相等,它们就不能被放在一起。
接下来,我们考虑如何统计答案。
我们先不考虑非法情况,那么总情况数就是 \(2^n\) 对吧。
为什么呢?
因为每种鱼都有选和不选两种情况。那如果将这个想法放在非法和合法混在一起的方案中呢?
那原来相乘的一坨鱼,就不能放在一起乘了,得分开然后相加。
那这样实现就非常容易了。
注意,有可能会有多条鱼约分后价值相同,为了方便于是就一起统计。
举个例子
没看懂上面那一大段文字可以直接看这里。
我们设有两种鱼 \(x\) 和 \(y\) 分别有 \(c\) 和 \(d\) 条,且这两条鱼的价值(定义如上)正好就是相反数取倒数的非法关系。
那么显然当前贡献就是:
吗?
其实还要减一,因为两边都是空集的情况被计算两次。
最终式子是这样的:
最后,讲一下实现。
开一个 \(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}\) 货物运输
假定我们要返回根,此时答案是好求的,即两倍的边权和。
而我们可以不返回根,故贪心地想,去掉最长的那条边折返所带来的代价。
实现上 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}\) 气温变化
比前面那个题还送。
题目要求一个滑动窗口的极差,可以用单调队列线段树之类的数据结构简单维护。
当然遇到这种动态维护最大最小值的个人认为 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
这个题的话两年前写的所以懒得拟题解了。
但是我觉得这个题比前面那个暴力题好,尽管前面可以锻炼一下数据结构但是没啥难度。
考虑定义 \(dp[i][j][k]\) 表示扫到第 \(i\) 位,\(a\) 取了 \(j\) 个,\(b\) 取了 \(k\) 个时的最小购买套餐份数。
原 \(\textup{T4}\) Bandit in a City
最大最小,一看就很二分。
确实有二分做法,好像是 \(O( N \cdot \log N )\) 做法。
这里给出树形 dp 做法。
状态定义:
其实也很好想到,我们将大问题拆成子问题去解决。
定义 \(dp_u\) 表示以 \(u\) 为根的子树中,其权值最大的叶子的最小的值。
状态转移和初始化:
我们计算出当前子树中儿子权值最大的数,并且贪心的将当前节点的权值进行分配。
由于为了最小化最大权值,我们可以尽量让叶子们得到的权值平均。
从而定义 \(sz_u\) 表示以 \(u\) 为根的子树中,有多少个叶子节点。
\(sum_u\) 表示以 \(u\) 为根的子树中,最终需要分配到其子树叶子节点的总权值。
先处理出:
并预处理 \(sz\) 和 \(sum\)。
考虑要尽量贪心地分配,于是有:
解释一下,就是将所有儿子所需要分配的人又重新平均分配了一遍。
但要与原来已经处理完子树取 \(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;
}


浙公网安备 33010602011771号