2024牛客暑假训练营5 (B E H K L 题) ✨✨✨
这一场感觉难度跨越太大,做对四个题运气好(bushi)(shi 够快够准)基本就是在100名左右了
这里也就不(you)刻(zi)意(zhi)去(zhi)攻(ming)克(ba)难(le)题(qwq),放一下四个简单题的题解和思路,然后就去刷刷别的题或者补其他题解去了
B 题
题目:用几张 1 × 2 1 \times 2 1×2或 2 × 1 2 \times 1 2×1的牌 覆盖一个 n × m n \times m n×m 的矩形。
此外,可能有两种类型的限制:
1.牌的短边不能相邻,也就是说没有两块多米诺骨牌可以共用一条长度为 1 的边。
2.牌的长边不能相邻,也就是说没有两块多米诺骨牌可以共用一条长度为 2 的边(即使它们只共用一条边)。
需要确定是否有办法覆盖整个矩形。
详细题目:B 题
分析:
典型的铺地砖问题,没什么好说的,就是找规律,这里放一下代码
这里要 注意 一下的就是 0 代表有限制,1 代表没有限制
代码:
#include <bits/stdc++.h>
using namespace std;
int n, m, a, b;
void solve(const int &Case)
{
cin >> n >> m >> a >> b;
if ((n * m) % 2 != 0) // 面积为奇数不行
cout << "No" << '\n';
else if (n * m == 2) // 只放一块砖可以
cout << "Yes" << '\n';
else if (a == 0 && b == 0)
{
cout << "No" << '\n';
}
else if (a == 1 && b == 0)
{
if ((m == 1 && n % 2 == 0) || (n == 1 && m % 2 == 0))
cout << "Yes" << '\n';
else
cout << "No" << '\n';
}
else if (a == 0 && b == 1)
{
if (n == 1 || m == 1)
cout << "No" << '\n';
else
cout << "Yes" << '\n';
}
else if (a == 1 && b == 1)
{
if ((n * m) % 2 == 0)
cout << "Yes" << '\n';
else
cout << "No" << '\n';
}
return;
}
int main(void)
{
ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
int T;
cin >> T;
while (T--)
solve(T);
return 0;
}
E 题
题目:梅和雷的骑士对决问题,要求梅的骑士存活数量最大
详细题目:E 题
分析:
是一个很经典的博弈问题,两方骑士对阵,而且都只能打对面的骑士,那这里就不存在什么策略血量问题
那如果还没有想来,就简单举几个例子
那如果两方有一对骑士血量相同,作为梅(先手),肯定先攻击占据优势,那雷继续攻击就只会浪费机会
作为雷,会攻击下一对血量相同的骑士来占据优势或者继续攻击血量比自己弱的骑士,
那为什么不趁这个机会攻击血量比自己高的敌方骑士?
=> 这个就和先手类似了,那如果雷去攻击血量比自己高的骑士,再攻击最多只会让双方骑士血量持平,梅会占据优势去攻击你的这方骑士,那这么看是完全没有意义的
那么对于这个题目,我们只需要关注每一对骑士的血量大小,额外注意血量相同的骑士个数就行
代码:
#include <bits/stdc++.h>
using namespace std;
using ll = long long;
const int N = 1e5 + 5;
ll arr[N];
ll brr[N];
void solve(const int &Case)
{
int n;
cin >> n;
for (int i = 1; i <= n; ++i)
cin >> arr[i];
for (int i = 1; i <= n; ++i)
cin >> brr[i];
ll ans = 0, cnt = 0;
for (int i = 1; i <= n; ++i)
{
if (arr[i] > brr[i])
ans++;
if (arr[i] == brr[i])
cnt++;
}
ans += (cnt + 1) / 2; // 向上取整
cout << ans << '\n';
return;
}
int main(void)
{
ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
int T;
cin >> T;
while (T--)
solve(T);
return 0;
}
H 题
题目:有一个无向图,其中每个顶点都有唯一的权重 a i a_i ai。
棋子放在一个顶点上。每一步会移动到权重最小的相邻顶点。如果所有相邻顶点的权重都大于当前顶点,则停止移动。
可以为顶点自由分配权重 a i a_i ai(确保它们都是唯一的),并选择棋子的初始位置。
目标使棋子访问的顶点数最大化。
详细题目:H 题
分析:
这个注意一下就是每次移动到权重最小的相邻顶点,说明走到下一个节点的时候,其父节点的相邻子节点都不能再走了
最后,最重要的就是这个写法,每走过一次,写成 vis[u]++,这样就会直观体现走了多少次
而不是 vis[i] =1,这样会导致回溯的时候使走过的标记点被撤掉,从而导致答案错误,或者运行超时。
代码:
#include <bits/stdc++.h>
using namespace std;
const int N = 44;
vector<int> G[N];
int vis[N];
int n, m;
int maxl;
void dfs(int st, int cnt)
{
maxl = max(maxl, cnt);
for (auto dst : G[st])
{
if (!vis[dst])
{
for (auto v : G[st])
vis[v]++;
dfs(dst, cnt + 1);
for (auto v : G[st])
vis[v]--;
}
}
return;
}
int main(void)
{
ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
cin >> n >> m;
while (m--)
{
int u, v;
cin >> u >> v;
G[u].push_back(v);
G[v].push_back(u);
}
for (int i = 1; i <= n; i++)
{
for (int j = 1; j <= n; j++)
vis[j] = 0;
vis[i]++;
dfs(i, 1);
}
cout << maxl << '\n';
return 0;
}
K题 (详细&补题)✨✨✨✨
题目: 爱丽丝和鲍勃玩猜数字游戏。
爱丽丝选择了一个整数 x x x。鲍勃问爱丽丝任意一个整数 y y y,爱丽丝会回答是或否,表示是否为 x ≥ y x \geq y x≥y。
此外,爱丽丝会记录每次查询的成本,即 ∣ x − y ∣ |x - y| ∣x−y∣。请注意,鲍勃并不知道与每次查询相关的具体成本。
鲍勃知道 x x x是按升序给出的整数 a 1 , a 2 , ⋯ , a n a_1, a_2, \cdots, a_n a1,a2,⋯,an之一。为了确定 x x x,鲍勃希望将查询成本总和的最坏情况降到最低。 请确定最坏情况下可能的最小查询成本总和。
详细题目:K题
分析:
推荐一篇不错的古老有趣的dp神章 从《鹰蛋》一题浅析对动态规划算法的优化,这个文章还没有细看,感觉看完还是有点不容易的,如果有机会说不定能出几篇关于阅读这个文章的感悟(是大坑!!!,溜~)
这个问题是一个求最坏情况下的最小值问题,大概有三个思路 => 二分,贪心和 dp,很明显只能是 dp 了
哦,对了,额外需要注意的就是,Bob 选数字猜的时候,选的数字不一定是数组里的
那我们回到正题,这个最小问题是什么,转移的变量又是什么?
问题: 在区间
[
i
,
j
]
[ i , j ]
[i,j] 之间猜数,距离猜到 Alice 给出的数字还需要多少代价(从后往前来)
于是动态规划的状态转移方程中开的数组先暂时确定为
d
p
[
i
]
[
j
]
dp[i][j]
dp[i][j],表示在区间
[
i
,
j
]
[ i , j ]
[i,j] 猜数字最坏情况在至少需要付出多少代价
对于下一个我们猜测的数字设置为k
但是因为我们不知道具体的数值,所以没有办法明确计算出每次的代价,
如果这个数字比
k
k
k 小,下一个区间为
[
i
,
k
−
1
]
[ i , k-1 ]
[i,k−1],那我们先认为这一步的代价为
j
−
k
+
1
j-k+1
j−k+1
如果这个数字比
k
k
k 大,下一个区间为
[
k
,
j
]
[ k , j ]
[k,j],认为这一步的代价为
k
−
i
k-i
k−i
那剩余部分的代价怎么办,嗯…怎么说,就是还是感觉要画个图,不然我这个匮乏的文字功底没办法说的很清楚
那关于代价的计算过程看图就行,记录各个状态的数组
d
p
[
i
]
[
j
]
dp[i][j]
dp[i][j] =>
d
p
[
i
]
[
j
]
[
x
]
[
y
]
dp[i][j][x][y]
dp[i][j][x][y]
=>
d
p
[
i
]
[
j
]
[
x
]
[
y
]
dp[i][j][x][y]
dp[i][j][x][y] 代表着 ans 在
[
a
r
r
i
,
a
r
r
j
]
[arri,arrj]
[arri,arrj] 的范围内,并且
a
r
r
i
arri
arri 的左边询问了
x
x
x 次,
a
r
r
j
arrj
arrj 右边询问了
y
y
y 次,最坏情况下最少还需要多少代价才能完成游戏
那事情还没有结束,后面还需要继续优化的地方是,将 d p [ i ] [ j ] [ x ] [ y ] dp[i][j][x][y] dp[i][j][x][y] 优化为三维数组,因为发现没有必要记录左右两边各猜测多少,只需要记录差值即可, d p [ i ] [ j ] [ x ] [ y ] dp[i][j][x][y] dp[i][j][x][y] => d p [ i ] [ j ] [ t ] dp[i][j][t] dp[i][j][t]
最后一步优化就是在
[
i
,
j
]
[ i , j ]
[i,j] 里面猜数字,现在时间复杂度为 O(cn^3),考虑怎么降低一维
转移里的
d
p
[
l
]
[
r
]
[
t
]
=
m
i
n
(
d
p
[
l
]
[
r
]
[
t
]
,
m
a
x
(
L
+
p
,
R
−
p
)
)
dp[l][r][t]=min(dp[l][r][t], max(L+p, R-p))
dp[l][r][t]=min(dp[l][r][t],max(L+p,R−p))
后面这个函数
f
(
x
)
=
m
a
x
(
L
+
x
,
R
−
x
)
f(x) = max(L+x, R-x)
f(x)=max(L+x,R−x) 关于
x
x
x 是单峰的
而且如果左端点不变,右端点向右延长,最优的
p
p
p 是会往右走的 (类比二分)
同理,右不动,左扩张的时候是向左走的
=> 这样就 最优点单调 了,就可以优化掉一维
到这里思路基本结束,说说代码里面的细节
int cal(int l, int r, int k, int c)
{
int x = f[l][k][c - 1], y = f[k + 1][r][c + 1];
int p = (y - x) / 2;
if (p < a[k] + 1)
p = a[k] + 1;
if (p > a[k + 1])
p = a[k + 1];
return max(x + p, y - p);
}
d
p
=
m
a
x
dp= max
dp=max 的转移,这里的转移是关于
p
p
p 单峰的
关于转移方程,询问的都是绝对值,如果询问在左边,贡献是负的,如果询问在右边,贡献是正的
每次只考虑这个询问带来的代价,
p
p
p 就是询问这个点带来的代价
d
p
[
l
]
[
r
]
[
t
]
=
m
i
n
(
d
p
[
l
]
[
r
]
[
t
]
,
m
a
x
(
L
+
p
,
R
−
p
)
)
dp[l][r][t]=min(dp[l][r][t],max(L+p,R-p))
dp[l][r][t]=min(dp[l][r][t],max(L+p,R−p))
=> 这里
m
a
x
max
max 是因为要考虑最坏情况,就是左右两边最差的一边
推出dp之后,如果暴力枚举决策点,时间复杂度为
O
(
c
.
n
2
.
∣
a
i
∣
)
O(c .n^2 .|ai|)
O(c.n2.∣ai∣)
但是这个函数
f
(
x
)
=
m
a
x
(
L
+
x
,
R
−
x
)
f(x) = max(L+x, R-x)
f(x)=max(L+x,R−x) 是先降后增的,或者没有增 没有降
相当于x取上边一半,所以在每个小区间里面都是V型的
这个函数 f(x) = max(L+x, R-x) 最低点很好找
比如计为X_0,那么在 (a[i],a[i+1]) 里,最小值一定在 {a[i]+1,a[i+1],x_0} 中
代码:
#include <bits/stdc++.h>
using namespace std;
#define int long long
#define endl '\n'
int n;
int a[1005];
int f[1005][1005][65];
int cal(int l, int r, int k, int c)
{
int x = f[l][k][c - 1], y = f[k + 1][r][c + 1];
int p = (y - x) / 2;
if (p < a[k] + 1)
p = a[k] + 1;
if (p > a[k + 1])
p = a[k + 1];
return max(x + p, y - p);
}
signed main()
{
cin >> n;
for (int i = 1; i <= n; i++)
cin >> a[i];
for (int l = n; l >= 1; --l)
{
for (int c = 1; c < 60; ++c)
{
f[l][l][c] = a[l] * (c - 30);
int k = l;
for (int r = l + 1; r <= n; ++r)
{
while (k + 1 < r && cal(l, r, k, c) > cal(l, r, k + 1, c))
++k;
f[l][r][c] = cal(l, r, k, c);
}
}
}
cout << f[1][n][30] << endl;
}
L 题
题目:游戏由 n n n个关卡组成,可以执行任意数量的操作。每个操作都允许选择一个索引 将 a i + 1 a_{i+1} ai+1递减一个,同时将 a i a_{i} ai递增一个。 目标是最大限度地提高通过所有关卡的概率。
详细题目:L 题
分析:
这个思路应该算是比较暴力一个,就是相邻两个数字取平均值,然后确定都为降序排列即可,
之前考虑过取总体平均值,但是这个思路牵扯很多问题,比如这个总体的平均值是向上还是向下取整,值比平均值多那么等于平均值,那比平均值少就不动,但是肯定要和上一个取平均得出结果是最大的,非常棘手而且本题数据范围小,这个思路感觉可行度不行就被放弃了
代码:
#include <bits/stdc++.h>
using namespace std;
using ll = long long;
ll mod = 998244353;
int a[105], n;
void solve(const int &Case)
{
ll s = 1;
cin >> n;
for (int i = 1; i <= n; i++)
cin >> a[i];
while (1)
{
int flag = 0;
for (int i = n - 1; i >= 1; i--)
{
if (a[i] < a[i + 1])
{
flag = 1;
int mid = a[i] + a[i + 1];
a[i] = mid / 2 + (mid % 2);
a[i + 1] = mid - a[i];
}
}
if (flag == 0)
break;
}
for (int i = 1; i <= n; i++)
s = (s * a[i]) % mod;
cout << s << '\n';
return;
}
int main(void)
{
ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
int T;
cin >> T;
while (T--)
solve(T);
return 0;
}