算法周赛笔记(7月第2周)— Acwing 第8场

说点废话

本狗学习算法也有一段时间了,不得不说,acwing的算法课讲的非常好,很对我的胃口,有一种相见恨晚的赶脚,如果能在刚上大学的时候遇见就好了!那我现在肯定变成大佬了(哭

还是忍不住想说一句:yxc牛逼!!!

基础算法的算法思路和模板代码学的都七七八八了,都基本敲了一遍(还剩动归和贪心的章节没学完)

确实像孔哥(我大学室友)说的,光看视频课,光自己刷题,是不行的,还是得刷周赛,有时间的限制和一种比较正式的仪式感。故本狗准备从这周开始,跟随孔哥步伐,开始参加算法周赛!并每周要把算法题目消化并整理笔记,以便回看和巩固。

本周共参加两场周赛,Acwing 和 LeetCode。
本篇是 对 acwing 本周周赛题目的笔记整理,如下

题目

Acwing - 3770

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

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 xy=axay 均成立。

例如,当有 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 124 号石子,它们的价值分别为 3 , 4 , 6 3,4,6 346 1 1 1 号石子与 2 2 2 号石子相邻, 2 − 1 = 4 − 3 2-1=4-3 21=43 成立。 2 2 2 号石子与 4 4 4 号石子相邻, 4 − 2 = 6 − 4 4-2=6-4 42=64 成立。所以方案合理。
  • 选择 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 xy=axay 进行一下变换,得 a x − x = a y − y a_x-x=a_y-y axx=ayy,这样以来,对于 i ∈ [ 1 , n ] i \in [1,n] i[1,n] ,每个石子 i i i 都可以计算出一个额外的属性 a i − i a_i-i aii,不妨令这个属性为 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

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,...,pk1),到终点 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 djdi+1 来进行更新,则每次遇到满足 d j ≥ d i + 1 d_j \ge d_i+1 djdi+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 djdi+1 条件,则说明 点 p 1 p_1 p1 到 点 p j p_j pj 的最短路有多条。

于是我们可以在每次遇到满足 d j ≥ d i + 1 d_j \ge d_i+1 djdi+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 ab 时 ,我们创建一条边 b → a b \rightarrow a ba ),方便从终点 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 didj+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的后两道还未消化完毕,笔记仍在整理中…

(完)

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 5
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值