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

·  阅读 249

说点废话

本狗学习算法也有一段时间了,不得不说,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: 选取石子

给定 nn 个石子,编号为 11nn

其中第 ii 个石子的价值为 aia_i

你需要从中任意挑选若干个石子,并将挑选好的石子按照编号从小到大的顺序排成一排。

选中的石子在排好序后需要满足,对于任意两个相邻的石子(不妨设它们的编号为 xxyy ),xy=axayx-y=a_x-a_y 均成立。

例如,当有 n=8n=8 个石子,石子价值分别为 [3,4,4,6,6,7,8,9][3,4,4,6,6,7,8,9],一些合理的选择方案如下:

  • 选择 1241,2,4 号石子,它们的价值分别为 3463,4,611 号石子与 22 号石子相邻,21=432-1=4-3 成立。22 号石子与 44 号石子相邻,42=644-2=6-4 成立。所以方案合理。
  • 选择 77 号石子。可以只选择一个石子,此时选取任何石子均为合理方案。

你的选择方案不仅需要合理,而且还要使得选中石子的价值总和尽可能大。

请计算并输出价值总和的最大可能值。

题解

我的解法: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;
}
复制代码

标准解法:哈希

xy=axayx-y=a_x-a_y 进行一下变换,得 axx=ayya_x-x=a_y-y,这样以来,对于 i[1,n]i \in [1,n] ,每个石子 ii 都可以计算出一个额外的属性 aiia_i-i,不妨令这个属性为 sis_i,则所有 sis_i 相同的石子,能够构成一种方案。则只需要对 sis_i 的每种取值,求解一下其中包含的石子的总价值即可。

#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: 更新线路

给定一个 nn 个点 mm 条边的有向强连通图。

点的编号为 11nn ,边的长度均为 11

给定一条由点 ss 到点 tt 的简单路径 p1,p2,...,pkp_1,p_2,...,p_k,其中 p1=sp_1=spk=tp_k=t

注意,这条路经不一定是从点 ss 到点 tt 的最短路径。

现在,小明要沿着这条路径从点 ss 走到点 tt

在他的行进过程中,手机上的导航软件将持续为他导航,持续为他提供最短行进线路建议。

当然,他并不一定会采纳这些建议,因为他一定会沿着之前给定的线路行进。

设想一下,在行进中,导航软件的工作过程。

首先,在点 ss 处,导航软件会找到并显示出一条从点 ss 到点 tt 的最短路径。

如果小明的行进线路恰好与软件推荐线路一致,则软件推荐线路将不会发生任何改变。

但是,如果小明在某一点处,行进线路与软件推荐线路发生了分歧,例如,软件推荐前往点 vv,小明却前往了点 ww

那么,在他到达点 ww 后,软件就会实时更新推荐线路,即找到并显示出一条从点 ww 到点 tt 的最短路径。

导航软件会一直工作到小明到达点 tt 为止,在这一过程中,软件的提供线路可能会经过若干次更新。

例如,给定一个有向强连通图,如下所示:

给出的简单路径为 [1,2,3,4](s=1,t=4)[1,2,3,4](s=1,t=4)

那么,小明从点 11 出发,导航软件找到并显示出一条从点 11 到点 44 的最短路径,这样的路径只有一条 [1,5,4][1,5,4]

小明并未听从软件的建议,坚持到达了点 22,此时软件推荐线路实时更新,提供出一条点 22 到点 44 的最短路径,例如 [2,6,4][2,6,4]

(注意,软件提供的最短路径也有可能是 [2,3,4][2,3,4])。

小明还是不听软件的建议,坚持到达了点 33,此时软件推荐线路再次更新,提供出一条点 33 到点 44 的最短路径,即 [3,4][3,4]

最后,小明沿软件提供路线,到达目的地点 44,软件完成导航。

总的来看,软件推荐线路发生了两次更新。

值得注意的是,如果软件在第一次更新推荐线路时,给出的最短路径为 [2,3,4][2,3,4],则小明将按照推荐线路走到终点,软件将无需再次更新推荐线路。

也就是说,由于软件在推荐最短路径时具有随机性,所以在整个行进过程中,软件更新推荐线路的次数并不确定。

现在,给定有向图和行进路线,请你求出软件更新推荐线路的最小可能次数和最大可能次数。

题解 根据题目,小明是一定会按照给出的路线 p1,p2,...,pkp_1,p_2,...,p_k 走下去的,关键就是求解出在途中的每个点(p1,p2,...,pk1p_1,p_2,...,p_{k-1}),到终点 pkp_k ,有多少条最短路。 我们考虑小明走一步的情况,即从 pip_i 走到 pi+1p_{i+1} 时,导航路线是否需要更新。

  • pip_ipi+1p_{i+1} 这条边,不在 pip_i 到终点 pkp_k 的某一条最短路径上时

    小明走到 pi+1p_{i+1} 时,导航路线一定会更新,所以更新路线的最小可能次数和最大可能次数都得加1。

    即,导航推荐的所有可能路线中,一定不包含 pip_ipi+1p_{i+1} 这条边,则小明走到 pi+1p_{i+1} 时,路线一定要发生更新。

  • pip_ipi+1p_{i+1} 这条边,pip_i 到终点 pkp_k 的某一条最短路径上时,此时要分情况讨论

    • pip_ipkp_k 的最短路径只有一条时

      这条最短路径一定是经过 pi+1p_{i+1} 这个点的。即,小明位于 pip_i 时,导航推荐的最短路线只有一种,那就是经过 pi+1p_{i+1} 的这条路线,那么小明走到 pi+1p_{i+1} 时,路线一定不会更新。

    • pip_ipkp_k 的最短路径多于一条时

      在小明位于 pip_i 时,若导航推荐的路线恰好经过 pi+1p_{i+1} ,则无需更新;若导航推荐的路线不是经过 pi+1p_{i+1} 的,则需要更新。即,此时最小可能次数不变,最大可能次数加1。

根据上面的讨论,我们知道了,关键在于,对于某个点 pip_i ,我们需要维护 pip_ipkp_k 的最短路的个数。

考虑用BFS来做,但是如果我们建一个正向图,即从点 p1p_1 开始进行BFS,则当扩展到 pip_i 时,只能得到 p1p_1pip_i 的最短路,也只能得到 p1p_1pip_i 的最短路个数。

求最短路的问题中,对于距离数组,我们是根据三角不等式来更新的 :dj>di+1d_j \gt d_i + 1。其中 ii 是从当前队列中拿出来的,与起点距离最短的点 pip_i,而 pjp_jpip_i 的相邻点。我们可以稍微变换一下,采用 djdi+1d_j \ge d_i+1 来进行更新,则每次遇到满足 djdi+1d_j \ge d_i+1 ,需要更新 djd_j 时,我们认为找到了一条 p1p_1 号点到 pjp_j 号点的最短路。对于同一个 jj,若多次遇到了 djdi+1d_j \ge d_i+1 条件,则说明 点 p1p_1 到 点 pjp_j 的最短路有多条。

于是我们可以在每次遇到满足 djdi+1d_j \ge d_i+1 时,都对 jj 进行计数,计数表示的是 p1p_1 号点到 pjp_j 号点的最短路的个数。(我自己的代码中考虑了重边的情况,即当存在重边时,不应当重复计数)

如果从 p1p_1 点开始BFS,则只能得到某个点 pjp_j 到点p1p_1 的最短路径的个数。而我们这道题目,需要用到的是某个点 pjp_j 到终点 pkp_k 的最短路径的个数。所以我们考虑建反向图(建反向图的意思是,当输入数据表明存在一条边 aba \rightarrow b 时 ,我们创建一条边 bab \rightarrow a ),方便从终点 pkp_k 开始进行BFS。

由于当BFS执行完毕后,对于最短路径上的每个点 pip_i 和点 pjp_j(假设 pip_ipjp_j 前面),都一定满足三角不等式 didj+1d_i \ge d_j+1

所以对于小明从 pip_i 走到 pi+1p_{i+1} 这一步,我们可以通过三角不等式来判断,pip_ipi+1p_{i+1} 这条边,是否在最短路上

如此,我们的思路就比较清晰了,关键点无非就是

  • 建反向图
  • 从终点 pkp_k 开始进行BFS,并额外维护某个点到 pkp_k 的最短路径的个数
  • 每次小明从 pip_i 走到 pi+1p_{i+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的后两道还未消化完毕,笔记仍在整理中....

(完)

分类:
后端
标签: