说点废话
本狗学习算法也有一段时间了,不得不说,acwing的算法课讲的非常好,很对我的胃口,有一种相见恨晚的赶脚,如果能在刚上大学的时候遇见就好了!那我现在肯定变成大佬了(哭
还是忍不住想说一句:yxc牛逼!!!
基础算法的算法思路和模板代码学的都七七八八了,都基本敲了一遍(还剩动归和贪心的章节没学完)
确实像孔哥(我大学室友)说的,光看视频课,光自己刷题,是不行的,还是得刷周赛,有时间的限制和一种比较正式的仪式感。故本狗准备从这周开始,跟随孔哥步伐,开始参加算法周赛!并每周要把算法题目消化并整理笔记,以便回看和巩固。
本周共参加两场周赛,Acwing 和 LeetCode。
本篇是 对 acwing 本周周赛题目的笔记整理,如下
题目
Acwing - 3770
有 n 个怪兽等待你去消灭。
怪兽共分为两种形态,不妨用 0 和 1 来表示。
消灭一个 0 形态的怪兽需要耗费的法力值为 a。
消灭一个 1 形态的怪兽需要耗费的法力值为 b。
你还可以使用改造魔法将 0 形态怪兽改造为 1 形态或将 1 形态怪兽改造为 0 形态。
改造一个怪兽需要耗费的法力值为 c。
请问,将怪兽全部消灭最少需要耗费多少法力值。
题解
签到题,不考察任何算法,直接模拟即可。对于0,要么直接消灭(消耗a),要么转变成1之后再消灭(消耗c+b)。消灭一个0形态的怪兽的最小代价就是min(a,c+b)
,同理,消灭一个1形态的怪兽的最小代价是min(b,c+a)
#include<iostream>
#include<cstring>
#include<algorithm>
using namespace std;
int get_min(int n, int a, int b, int c, string s) {
int m_0 = min(a, c + b);
int m_1 = min(b, c + a);
int res = 0;
for(int i = 0; i < s.size(); i++) {
if(s[i] == '0') res += m_0;
else res += m_1;
}
return res;
}
int main() {
int m;
scanf("%d", &m);
while(m--) {
int n, a, b, c;
scanf("%d%d%d%d", &n, &a, &b, &c);
string s;
cin >> s;
printf("%d\n", get_min(n, a, b, c, s));
}
return 0;
}
Acwing - 3771
给定 n n n 个石子,编号为 1 1 1 ∼ n n n。
其中第 i i i 个石子的价值为 a i a_i ai。
你需要从中任意挑选若干个石子,并将挑选好的石子按照编号从小到大的顺序排成一排。
选中的石子在排好序后需要满足,对于任意两个相邻的石子(不妨设它们的编号为 x x x, y y y ), x − y = a x − a y x-y=a_x-a_y x−y=ax−ay 均成立。
例如,当有 n = 8 n=8 n=8 个石子,石子价值分别为 [ 3 , 4 , 4 , 6 , 6 , 7 , 8 , 9 ] [3,4,4,6,6,7,8,9] [3,4,4,6,6,7,8,9],一些合理的选择方案如下:
- 选择 1 , 2 , 4 1,2,4 1,2,4 号石子,它们的价值分别为 3 , 4 , 6 3,4,6 3,4,6。 1 1 1 号石子与 2 2 2 号石子相邻, 2 − 1 = 4 − 3 2-1=4-3 2−1=4−3 成立。 2 2 2 号石子与 4 4 4 号石子相邻, 4 − 2 = 6 − 4 4-2=6-4 4−2=6−4 成立。所以方案合理。
- 选择 7 7 7 号石子。可以只选择一个石子,此时选取任何石子均为合理方案。
你的选择方案不仅需要合理,而且还要使得选中石子的价值总和尽可能大。
请计算并输出价值总和的最大可能值。
题解
我的解法:DFS暴搜
貌似能求出正确答案,但是会报 Time Limit Exceeded
#include<iostream>
using namespace std;
typedef long long LL;
const int N = 1e6;
int a[N], n;
LL res = 0, ans = 0;
int path[N], ctn; //存储已经纳入的点
// 每次枚举第x编号的石子, 都有2种选择, 是否纳入
void dfs(int x) {
if(x > n) return; // 深搜结束
// 不纳入, 直接深搜下一个节点
dfs(x + 1);
bool flag = false;
// 尝试纳入, 检查该次纳入是否合法
// 当前已存在纳入的石子
if(ctn > 0) {
int pre = path[ctn - 1]; // 获取当前最后一个石子
if(x - pre == a[x] - a[pre]) {
// 符合纳入条件
path[ctn++] = x;
res += a[x];
flag = true;
} else return; // 不符合纳入条件, 直接剪枝
} else {
// 当前已纳入的石子为0, 直接纳入
path[ctn++] = x;
res += a[x];
flag = true;
}
// 纳入后记录答案
ans = max(ans, res);
dfs(x + 1); // 深搜下一个位置
// 深搜完毕后, 恢复现场
if(flag) {
ctn--;
res -= a[x];
}
}
int main() {
scanf("%d", &n);
for(int i = 1; i <= n; i++) scanf("%d", &a[i]);
dfs(1);
printf("%lld", ans);
return 0;
}
标准解法:哈希
将 x − y = a x − a y x-y=a_x-a_y x−y=ax−ay 进行一下变换,得 a x − x = a y − y a_x-x=a_y-y ax−x=ay−y,这样以来,对于 i ∈ [ 1 , n ] i \in [1,n] i∈[1,n] ,每个石子 i i i 都可以计算出一个额外的属性 a i − i a_i-i ai−i,不妨令这个属性为 s i s_i si,则所有 s i s_i si 相同的石子,能够构成一种方案。则只需要对 s i s_i si 的每种取值,求解一下其中包含的石子的总价值即可。
#include<iostream>
#include<unordered_map>
using namespace std;
typedef long long LL;
const int N = 1e6;
int a[N];
int main() {
unordered_map<int,LL> res;
int n;
scanf("%d", &n);
LL ans = 0;
for(int i = 1; i <= n; i++) {
scanf("%d", &a[i]);
res[a[i] - i] += a[i]; // 对于 a[i] - i 这一组的石子,进行价值累加
ans = max(ans, res[a[i] - i]); // 实时保存当前价值最大的组的价值总和
}
printf("%lld", ans);
return 0;
}
Acwing - 3772
给定一个 n n n 个点 m m m 条边的有向强连通图。
点的编号为 1 1 1 ∼ n n n ,边的长度均为 1 1 1。
给定一条由点 s s s 到点 t t t 的简单路径 p 1 , p 2 , . . . , p k p_1,p_2,...,p_k p1,p2,...,pk,其中 p 1 = s p_1=s p1=s, p k = t p_k=t pk=t。
注意,这条路经不一定是从点 s s s 到点 t t t 的最短路径。
现在,小明要沿着这条路径从点 s s s 走到点 t t t。
在他的行进过程中,手机上的导航软件将持续为他导航,持续为他提供最短行进线路建议。
当然,他并不一定会采纳这些建议,因为他一定会沿着之前给定的线路行进。
设想一下,在行进中,导航软件的工作过程。
首先,在点 s s s 处,导航软件会找到并显示出一条从点 s s s 到点 t t t 的最短路径。
如果小明的行进线路恰好与软件推荐线路一致,则软件推荐线路将不会发生任何改变。
但是,如果小明在某一点处,行进线路与软件推荐线路发生了分歧,例如,软件推荐前往点 v v v,小明却前往了点 w w w。
那么,在他到达点 w w w 后,软件就会实时更新推荐线路,即找到并显示出一条从点 w w w 到点 t t t 的最短路径。
导航软件会一直工作到小明到达点 t t t 为止,在这一过程中,软件的提供线路可能会经过若干次更新。
例如,给定一个有向强连通图,如下所示:
给出的简单路径为 [ 1 , 2 , 3 , 4 ] ( s = 1 , t = 4 ) [1,2,3,4](s=1,t=4) [1,2,3,4](s=1,t=4)
那么,小明从点 1 1 1 出发,导航软件找到并显示出一条从点 1 1 1 到点 4 4 4 的最短路径,这样的路径只有一条 [ 1 , 5 , 4 ] [1,5,4] [1,5,4]。
小明并未听从软件的建议,坚持到达了点 2 2 2,此时软件推荐线路实时更新,提供出一条点 2 2 2 到点 4 4 4 的最短路径,例如 [ 2 , 6 , 4 ] [2,6,4] [2,6,4]
(注意,软件提供的最短路径也有可能是 [ 2 , 3 , 4 ] [2,3,4] [2,3,4])。
小明还是不听软件的建议,坚持到达了点 3 3 3,此时软件推荐线路再次更新,提供出一条点 3 3 3 到点 4 4 4 的最短路径,即 [ 3 , 4 ] [3,4] [3,4]。
最后,小明沿软件提供路线,到达目的地点 4 4 4,软件完成导航。
总的来看,软件推荐线路发生了两次更新。
值得注意的是,如果软件在第一次更新推荐线路时,给出的最短路径为 [ 2 , 3 , 4 ] [2,3,4] [2,3,4],则小明将按照推荐线路走到终点,软件将无需再次更新推荐线路。
也就是说,由于软件在推荐最短路径时具有随机性,所以在整个行进过程中,软件更新推荐线路的次数并不确定。
现在,给定有向图和行进路线,请你求出软件更新推荐线路的最小可能次数和最大可能次数。
题解
根据题目,小明是一定会按照给出的路线
p
1
,
p
2
,
.
.
.
,
p
k
p_1,p_2,...,p_k
p1,p2,...,pk 走下去的,关键就是求解出在途中的每个点(
p
1
,
p
2
,
.
.
.
,
p
k
−
1
p_1,p_2,...,p_{k-1}
p1,p2,...,pk−1),到终点
p
k
p_k
pk ,有多少条最短路。
我们考虑小明走一步的情况,即从
p
i
p_i
pi 走到
p
i
+
1
p_{i+1}
pi+1 时,导航路线是否需要更新。
-
当 p i p_i pi 到 p i + 1 p_{i+1} pi+1 这条边,不在 p i p_i pi 到终点 p k p_k pk 的某一条最短路径上时
小明走到 p i + 1 p_{i+1} pi+1 时,导航路线一定会更新,所以更新路线的最小可能次数和最大可能次数都得加1。
即,导航推荐的所有可能路线中,一定不包含 p i p_i pi 到 p i + 1 p_{i+1} pi+1 这条边,则小明走到 p i + 1 p_{i+1} pi+1 时,路线一定要发生更新。
-
当 p i p_i pi 到 p i + 1 p_{i+1} pi+1 这条边,在 p i p_i pi 到终点 p k p_k pk 的某一条最短路径上时,此时要分情况讨论
-
当 p i p_i pi 到 p k p_k pk 的最短路径只有一条时
这条最短路径一定是经过 p i + 1 p_{i+1} pi+1 这个点的。即,小明位于 p i p_i pi 时,导航推荐的最短路线只有一种,那就是经过 p i + 1 p_{i+1} pi+1 的这条路线,那么小明走到 p i + 1 p_{i+1} pi+1 时,路线一定不会更新。
-
当 p i p_i pi 到 p k p_k pk 的最短路径多于一条时
在小明位于 p i p_i pi 时,若导航推荐的路线恰好经过 p i + 1 p_{i+1} pi+1 ,则无需更新;若导航推荐的路线不是经过 p i + 1 p_{i+1} pi+1 的,则需要更新。即,此时最小可能次数不变,最大可能次数加1。
-
根据上面的讨论,我们知道了,关键在于,对于某个点 p i p_i pi ,我们需要维护 p i p_i pi 到 p k p_k pk 的最短路的个数。
考虑用BFS来做,但是如果我们建一个正向图,即从点 p 1 p_1 p1 开始进行BFS,则当扩展到 p i p_i pi 时,只能得到 p 1 p_1 p1 到 p i p_i pi 的最短路,也只能得到 p 1 p_1 p1 到 p i p_i pi 的最短路个数。
求最短路的问题中,对于距离数组,我们是根据三角不等式来更新的 : d j > d i + 1 d_j \gt d_i + 1 dj>di+1。其中 i i i 是从当前队列中拿出来的,与起点距离最短的点 p i p_i pi,而 p j p_j pj 是 p i p_i pi 的相邻点。我们可以稍微变换一下,采用 d j ≥ d i + 1 d_j \ge d_i+1 dj≥di+1 来进行更新,则每次遇到满足 d j ≥ d i + 1 d_j \ge d_i+1 dj≥di+1 ,需要更新 d j d_j dj 时,我们认为找到了一条 p 1 p_1 p1 号点到 p j p_j pj 号点的最短路。对于同一个 j j j,若多次遇到了 d j ≥ d i + 1 d_j \ge d_i+1 dj≥di+1 条件,则说明 点 p 1 p_1 p1 到 点 p j p_j pj 的最短路有多条。
于是我们可以在每次遇到满足 d j ≥ d i + 1 d_j \ge d_i+1 dj≥di+1 时,都对 j j j 进行计数,计数表示的是 p 1 p_1 p1 号点到 p j p_j pj 号点的最短路的个数。(我自己的代码中考虑了重边的情况,即当存在重边时,不应当重复计数)
如果从 p 1 p_1 p1 点开始BFS,则只能得到某个点 p j p_j pj 到点 p 1 p_1 p1 的最短路径的个数。而我们这道题目,需要用到的是某个点 p j p_j pj 到终点 p k p_k pk 的最短路径的个数。所以我们考虑建反向图(建反向图的意思是,当输入数据表明存在一条边 a → b a \rightarrow b a→b 时 ,我们创建一条边 b → a b \rightarrow a b→a ),方便从终点 p k p_k pk 开始进行BFS。
由于当BFS执行完毕后,对于最短路径上的每个点 p i p_i pi 和点 p j p_j pj(假设 p i p_i pi 在 p j p_j pj 前面),都一定满足三角不等式 d i ≥ d j + 1 d_i \ge d_j+1 di≥dj+1
所以对于小明从 p i p_i pi 走到 p i + 1 p_{i+1} pi+1 这一步,我们可以通过三角不等式来判断, p i p_i pi 到 p i + 1 p_{i+1} pi+1 这条边,是否在最短路上
如此,我们的思路就比较清晰了,关键点无非就是
- 建反向图
- 从终点 p k p_k pk 开始进行BFS,并额外维护某个点到 p k p_k pk 的最短路径的个数
- 每次小明从 p i p_i pi 走到 p i + 1 p_{i+1} pi+1 时,可以根据三角不等式来判断这一步是否在最短路径上
我的解法:
#include<iostream>
#include<cstring>
using namespace std;
const int N = 2e5 + 10, M = N;
const int INF = 0x3f3f3f3f;
int h[N], e[N], ne[N], idx; // 图的邻接表存储
int ctn[N]; // 存储某个点 i 到终点 k 的最短路径的个数
int n, m, k;
int path[N]; // 给定的路线
int d[N];
int q[N]; // 队列
bool st[N]; // 用于防止重边时多次对 ctn 进行计数
bool stt[N]; // 判断一个节点是否在队列q中
void add(int a, int b) {
// 邻接链表, 头插法
e[idx] = b;
ne[idx] = h[a];
h[a] = idx++;
}
void bfs() {
memset(d, 0x3f, sizeof d);
d[path[k]] = 0;
stt[path[k]] = true; // stt表示是否添加到队列
int hh = 0, tt = -1;
q[++tt] = path[k]; // 入队
while(tt >= hh) {
// 当队列非空
int t = q[hh++]; // 出队
stt[t] = false;
memset(st, false, sizeof st); // 用于防止重边, 添加多个 ctn
// 遍历所有出边
for(int i = h[t]; i != -1; i = ne[i]) {
int j = e[i];
if(d[j] >= d[t] + 1) {
// 发生更新
d[j] = d[t] + 1;
if(!st[j]) {
// j 点没有被走过时, 添加ctn (防止重边添加多次 ctn)
ctn[j]++;
st[j] = true;
}
if(!stt[j]) {
q[++tt] = j; // 入队
stt[j] = true;
}
}
}
}
}
int main() {
memset(h, -1, sizeof h);
scanf("%d%d", &n, &m);
while(m--) {
int a, b;
scanf("%d%d", &a, &b);
add(b, a); // 建反向边
}
scanf("%d", &k);
for(int i = 1; i <= k; i++) scanf("%d", &path[i]);
bfs(); // 先进行宽搜, 建立数据
int min_c = 0, max_c = 0; // 更新路线的最小可能次数和最大可能次数
// 进行处理
for(int i = 1; i < k; i++) {
int a = path[i], b = path[i + 1];
// 查看 i 到 i + 1 这条边是否在最短路上
// 由于建的是反向图, BFS从 k 开始, 所以BFS时, 点 i + 1 在 i 的前面被搜到
if(d[a] < d[b] + 1) {
// 说明 i 到 i + 1 不在最短路上, 路线一定会发生更新
min_c++;
max_c++;
} else {
// i 到 i + 1 在最短路上, 看一下最短路有多少条
if(ctn[a] > 1) {
// i 点 到 k 点 的最短路个数大于1, 即不止当前这个 i 到 i + 1 这条最短路
max_c++;
}
}
}
printf("%d %d\n", min_c, max_c);
return 0;
}
yxc标准解法:
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 200010, M = N;
int n, m;
int h[N], e[M], ne[M], idx;
int dist[N], cnt[N], q[N];
int path[N];
void add(int a, int b) // 添加一条边a->b
{
e[idx] = b, ne[idx] = h[a], h[a] = idx ++ ;
}
void bfs(int start)
{
int hh = 0, tt = 0;
memset(dist, 0x3f, sizeof dist);
dist[start] = 0;
q[0] = start;
while (hh <= tt)
{
int t = q[hh ++ ];
for (int i = h[t]; ~i; i = ne[i])
{
int j = e[i];
if (dist[j] > dist[t] + 1)
{
dist[j] = dist[t] + 1;
cnt[j] = 1;
q[ ++ tt] = j;
}
else if (dist[j] == dist[t] + 1)
cnt[j] ++ ;
}
}
}
int main()
{
scanf("%d%d", &n, &m);
memset(h, -1, sizeof h);
while (m -- )
{
int a, b;
scanf("%d%d", &a, &b);
add(b, a);
}
int k;
scanf("%d", &k);
for (int i = 1; i <= k; i ++ ) scanf("%d", &path[i]);
bfs(path[k]);
int minc = 0, maxc = 0;
for (int i = 1; i < k; i ++ )
{
int a = path[i], b = path[i + 1];
if (dist[a] < dist[b] + 1) minc ++, maxc ++ ;
else if (cnt[a] > 1) maxc ++ ;
}
printf("%d %d\n", minc, maxc);
return 0;
}
本周是第一次参加周赛,战况不是很理想。Acwing 一共3道题,只做出第一道,第二题没有想到用哈希。
LeetCode 一共4道,只做出前2道,并且LeetCode的后两道还未消化完毕,笔记仍在整理中…
(完)