动态规划真题 Dynamic Programming

P11200 [JOIG 2024] 座席 2 / Seats 2

题面 洛谷链接可查

P11200 [JOIG 2024] 座席 2 / Seats 2

题目描述

今年,JOI 国将主办 IOI(国际信息学奥林匹克竞赛)。届时将有 \(N\) 名选手参赛,编号从 \(1\)\(N\)

每位选手的国籍由一个介于 \(1\)\(10^9\) 之间的整数表示:选手 \(i(1\le i\le N)\) 来自国家 \(C_i\)。保证 \(N\) > > 个选手的国籍不完全相同(即存在 \(i\ne j(1\le i,j\le N)\) 使得 \(C_i\ne C_j\))。

选手的座位排成一条直线,选手 \(i(1\le i\le N)\) 的座位在 \(X_i\) 处。选手 \(i(1\le i\le N)\) 和选手 \(j(1\le > j\le N)\) 之间的座位距离\(|X_i-X_j|\)

每个选手都想知道在与其他选手交流时,与离自己最近的异国选手的座位距离。

给定每个选手的国籍和座位位置,请为每个选手 \(i(1\le i\le N)\) 求出与其来自不同国家的选手中,座位离选手 \(i\) >最近的选手与 \(i\) 的座位距离。

输入格式

第一行输入一个整数 \(N\)

接下来 \(N\) 行,每行输入两个整数 \(C_i,X_i\)

输出格式

输出 \(N\) 行,第 \(i\) 行输出一个整数,表示座位离选手 \(i\) 最近的选手与 \(i\) 的座位距离。

输入输出样例 #1
输入 #1

3
2 5
1 1
1 2

输出 #1

3
4
3

输入输出样例 #2
输入 #2

5
1 1
2 4
2 14
3 10
2 2

输出 #2

1
3
4
4
1

输入输出样例 #3
输入 #3

3
1 1
2 1
1 1

输出 #3

0
0
0

说明/提示

【样例解释 #1】

  • 选手 \(1\) 来自国家 \(2\),选手 \(2, 3\) 和他 / 她来自不同国家。在这些选手中,与选手 \(1\) 座位距离最小的是选手 \(3\),座位距离为 \(3\)。因此,答案为 \(3\)
  • 选手 \(2\) 来自国家 \(1\),选手 \(1\) 是唯一和他 / 她来自不同国家的选手。选手 \(2\) 和选手 \(1\) 之间的座位距离为 \(4\)
  • 选手 \(3\) 来自国家 \(1\),选手 \(1\) 是唯一和他 / 她来自不同国家的选手。选手 \(3\) 和选手 \(1\) 之间的座位距离为 \(3\)

该样例满足子任务 \(1,2,3\) 的限制。

【样例解释 #2】

该样例满足子任务 \(1,2,3\) 的限制。

【样例解释 #3】

该样例满足子任务 \(1,2,3\) 的限制。

【数据范围】

  • \(2\le N \le 3\times 10^5\)
  • \(1\le C_i\le 10^9(1\le i\le N)\)\(C_i\) 不完全相同;
  • \(1\le X_i\le 10^9(1\le i\le N)\)

【子任务】

  1. \(20\) 分)\(N\le 1000\)
  2. \(40\) 分)\(C_i\le 10(1\le i\le N)\)
  3. \(40\) 分)无附加限制。

分析

首先看到这道题,首先想的是暴力枚举,思路大概是这样的:

  1. 初始:给所有选手座位排个序,方便后面操作。
  2. 遍历:循环 1 到 n 号选手,然后以这个选手固定往左右查找第一个不是跟自己同一个国家的选手。
  3. 答案:找到每个选手的最小距离,然后按序输出。

经过一些运算即可发现,这个思路的时间复杂度是 \(O(n^2)\) ,对于 $ 3 \times 10^5 $ 是肯定无法通过。

那可以怎么优化呢?先看一下我们优化之后的代码:

  1. 初始化:排序,也是方便处理。
  2. 预处理:左右最近的不同国家选手索引,大概就是相同记录前一个选手的索引,不同就是我们需要的答案。
  3. 算答案:根据索引就可以直接获取并计算答案了。

无后效性、最优子结构、空间换时间,想到了什么没有?没错,就是动态规划。

让我们再看一下对比:

维度 动态规划 暴力枚举
时间复杂度 $ O(n \log n) $(快速排序占主要时间) $ O(n^2) $(每个选手遍历左右所有元素)
预处理 两次线性遍历预处理左右索引,O(n) 无预处理,每次从头搜索
查询效率 O(1) 直接取预处理结果 O(n) 每次线性扫描左右
适用数据规模 可处理 \(n \le 3e5\) 的大数据 仅适用于小数据(如 \(n \le 1e4\)
空间复杂度 O(n) 存储左右索引和结果 O(1) 无额外空间(但时间不可接受)

可以看到,动态规划在时间上远远优秀于暴力枚举。

接下来,我们就带入样例一给大家展示一下

预处理结果:

  • l 数组(左异国索引):

    • l[2] = -1(国家与前一个相同)
    • l[3] = 2(国家与前一个不同)
    • l[4] = 3(继承 l[3]
    • l[5] = 4(国家与前一个不同)
  • r 数组(右异国索引):

    • r[2] = 3(国家与后一个不同)
    • r[3] = 5(国家与后一个相同,继承 r[4]
    • r[4] = 5(国家与后一个不同)

计算示例(索引3的选手):

  • 左距离:a[2].x - a[3].x = 90 - 80 = 10
  • 右距离:a[3].x - a[5].x = 80 - 60 = 20
  • 最小距离:min(10, 20) = 10

这就是代码的运行结构,比较浅显易懂,对吧。

代码

#include <bits/stdc++.h>
using namespace std;
const int N = 3e5 + 10;

struct P {
	int c, x, id;
} a[N];
int n;

bool cmp(P u, P v) {
	return u.x > v.x;
}

int l[N], r[N], ans[N];

int main() {
	scanf("%d", &n);
	for (int i = 1; i <= n; i++) {
		scanf("%d %d", &a[i].c, &a[i].x);
		a[i].id = i;
	}
	sort(a + 1, a + n + 1, cmp);

	memset(l, -1, sizeof(l));
	memset(r, -1, sizeof(r));

	//Pretreatment
	for (int i = 2; i <= n; i++) {
		if (a[i].c != a[i - 1].c)
			l[i] = i - 1;
		else
			l[i] = l[i - 1];
	}

	for (int i = n - 1; i >= 1; i--) {
		if (a[i].c != a[i + 1].c)
			r[i] = i + 1;
		else
			r[i] = r[i + 1];
	}
	
	//Query
	for (int i = 1; i <= n; i++) {
		int mn = INT_MAX;
		if (l[i] != -1)
			mn = a[l[i]].x - a[i].x;
		if (r[i] != -1)
			mn = min(mn, a[i].x - a[r[i]].x);
		ans[a[i].id] = mn;
	}

	for (int i = 1; i <= n; i++)
		printf("%d\n", ans[i]);

	return 0;
}

总结

有时候做题时,可以不用可以去想正解的做法,因为正解可能就藏在普通代码的优化上,就例如这道题,如果直接去想动态规划的代码是很难联系在一起的,所以做题要按部就班,而不是一蹴而就。

P11246 [GESP202409 六级] 小杨和整数拆分

题面 洛谷链接可查

P11246 [GESP202409 六级] 小杨和整数拆分

题目描述

小杨有一个正整数 \(n\),小杨想将它拆分成若干完全平方数的和,同时小杨希望拆分的数量越少越好。

编程计算总和为 \(n\) 的完全平方数的最小数量。

输入格式

输入只有一行一个正整数 \(n\)

输出格式

输出一行一个整数表示答案。

输入输出样例 #1
输入 #1

18

输出 #1

2

说明/提示
数据规模与约定

对全部的测试数据,保证 \(1 \leq n \leq 10^5\)

分析

看到这题,我们很快就能想到一个动态规划思路。

对于一个数 \(i\) :

  • 要么通过不断累加 1 得到它

  • 要么通过上一个数(间隔为 \(i\)) 加上一个完全平方数(转换为 $j \times j $ )得到它

再在中间取一个步数更小的方案。

然后我们就得到了状态转移方程:

$ dp_i = min( dp_i , dp_{i-j \times j } + 1 ) $

一开始的时候我们也需要给每个 $ dp_i $ 赋值 $ i $

因为最坏情况就是不断累加 1 得到。

由此我们就得到了代码。

代码

/*
Range: n<=1e5
*/
#include <bits/stdc++.h>
using namespace std;
const int MAXN = 1e5 + 10;
int n, dp[MAXN];

int main() {
	scanf("%d", &n);
	for (int i = 1; i <= n; i++)
		dp[i] = i;
	for (int i = 1; i <= n; i++)
		for (int j = 1; j * j <= i; j++)
			dp[i] = min(dp[i], dp[i - j * j] + 1);
	printf("%d", dp[n]);
	return 0;
}

总结

这道题也是比较经典的动态规划,这种动态规划是入门的好题,适合初学者多刷。

P11377 [GESP202412 七级] 武器购买

题面 洛谷链接可查

P11377 [GESP202412 七级] 武器购买

题目描述

商店里有 \(n\) 个武器,第 \(i\) 个武器的强度为 \(p_i\),花费为 \(c_i\)

小杨想要购买一些武器,满足这些武器的总强度不小于 \(P\),总花费不超过 \(Q\),小杨想知道是否存在满足条件的购买方案,如果有,最少花费又是多少。

输入格式

第一行包含一个正整数 \(t\),代表测试数据组数。

对于每组测试数据,第一行包含三个正整数 \(n,P,Q\),含义如题面所示。

之后 \(n\) 行,每行包含两个正整数 \(p_i,c_i\),代表武器的强度和花费。

输出格式

对于每组测试数据,如果存在满条件的购买方案,输出最少花费,否则输出 -1

输入输出样例 #1
输入 #1

3
3 2 3
1 2
1 2
2 3
3 3 4
1 2
1 2
2 3
3 1000 1000
1 2
1 2
2 3

输出 #1

3
-1
-1

说明/提示
子任务编号 数据点占比 \(n\) \(p_i\) \(c_i\) \(P\) \(Q\)
\(1\) \(20\%\) \(\leq 10\) \(1\) \(1\) \(\leq 10\) \(\leq 10\)
\(2\) \(20\%\) \(\leq 100\) \(\leq 5\times 10^4\) \(1\) \(\leq 5\times 10^4\) \(2\)
\(3\) \(60\%\) \(\leq 100\) \(\leq 5\times 10^4\) \(\leq 5\times 10^4\) \(\leq 5\times 10^4\) \(\leq 5\times 10^4\)

对于全部数据,保证有 \(1\leq t\leq 10\)\(1\leq n\leq 100\)\(1\leq p_i,c_i,P,Q\leq 5\times 10^4\)

分析

仔细读过题目发现属于 01背包(参考OI-Wiki) 或者看 P1048采药 都可以。

可以尝试使用一维优化。

主要是最后部分,由于需要大于等于目标值 \(P\) ,同时花费不小于 \(Q\)

我们考虑直接从 DP 数组中从前往后搜,因为前面的花费肯定比后面的小,搜到的第一个大于等于目标值的数就是我们这道题的答案,同时多组数据记得初始化数组

代码

#include <bits/stdc++.h>
using namespace std;
const int MAXN = 5e4 + 10;
const int INF = 1e9;
int dp[MAXN],arr[MAXN][2];

int main() {
    int t;
    cin >> t;
    while (t--) {
        int n, P, Q;
        cin >> n >> P >> Q;
        for (int i = 0; i < n; ++i) {
            cin >> arr[i][0] >> arr[i][1];
        }
        for (int j = 0; j <= Q; ++j) {
            dp[j] = -INF;
        }
        dp[0] = 0;
        for (int i = 0; i < n; ++i) {
            for (int j = Q; j >= c; --j) {
                dp[j] = (dp[j - arr[i][1]] + arr[i][0] > dp[j])?dp[j - arr[i][1]] + arr[i][0]:dp[j];
            }
        }
        int minn = INF;
        for (int j = 0; j <= Q; ++j) {
            if (dp[j] >= P && j < minn) {
                minn = j;
            }
        }
        if (minn != INF) {
            cout << minn << endl;
        } else {
            cout << -1 << endl;
        }
        delete[] dp;
    }
    return 0;
}

总结

这道题比较考虑细心,难度倒不是很大,所以写完一道题后记得检查以免出错。

P11376 [GESP202412 六级] 运送物资

题面 洛谷链接可查

P11376 [GESP202412 六级] 运送物资

题目描述

小杨管理着 \(m\) 辆货车,每辆货车每天需要向 A 市和 B 市运送若干次物资。小杨同时拥有 \(n\) 个运输站点,这些站点位于 A 市和 B 市之间。

每次运送物资时,货车从初始运输站点出发,前往 A 市或 B 市,之后返回初始运输站点。A 市、B 市和运输站点的位置可以视作数轴上的三个点,其中 A 市的坐标为 \(0\),B 市的坐标为 \(x\),运输站点的坐标为 \(p\) 且有 \(0 \lt p \lt x\)。货> 车每次去 A 市运送物资的总行驶路程为 \(2p\),去 B 市运送物资的总行驶路程为 \(2(x - p)\)

对于第 \(i\) 个运输站点,其位置为 \(p_i\) 且至多作为 \(c_i\) 辆车的初始运输站点。小杨想知道,在最优分配每辆货车的初始运输站点的情况下,所有货车每天的最短总行驶路程是多少。

输入格式

第一行包含三个正整数 \(n,m,x\),代表运输站点数量、货车数量和两市距离。

之后 \(n\) 行,每行包含两个正整数 \(p_i\)\(c_i\),代表第 \(i\) 个运输站点的位置和最多容纳车辆数。

之后 \(m\) 行,每行包含两个正整数 \(a_i\)\(b_i\),代表第 \(i\) 辆货车每天需要向 A 市运送 \(a_i\) 次物资,向 B 市运送 \(b_i\) 次物资。

输出格式

输出一个正整数,代表所有货车每天的最短总行驶路程。

输入输出样例 #1
输入 #1

3 4 10
1 1
2 1
8 3
5 3
7 2
9 0
1 10000

输出 #1

40186

说明/提示

\(1\) 辆车的初始运输站点为站点 \(3\),第 \(2\) 辆车的初始运输站点为站点 \(2\)。第 \(3\) 辆车的初始运输站点为站点 \(1\),第 \(4\) 辆车的初始运输站点为站点 \(3\)。此时总驶路程最短,为 \(40186\)

子任务编号 数据点占比 \(n\) \(s\) \(c_i\)
\(1\) \(20\%\) \(2\) \(2\) \(1\)
\(2\) \(20\%\) \(\leq 10^5\) \(\leq 10^5\) \(1\)
\(3\) \(60\%\) \(\leq 10^5\) \(\leq 10^5\) \(\leq 10^5\)

对于全部数据,保证有 \(1\leq n,m\leq 10^5\)\(2\leq x\leq 10^8\)\(0\lt p_i\lt x\)\(1\leq c_i\leq 10^5\)\(0\leq a_i,b_i\leq 10^5\)。数据保证 \(\sum c_i\geq m\)

分析

题目是说有货车要在A市和B市之间来回送货。每个货车每天得往A市跑\(a_i\)次,往B市跑\(b_i\)次。中间有几个运输站点,每辆货车得选一个站点作为起点。假设A市坐标是\(0\),B市坐标是\(x\),站点坐标p满足\(0 < p < x\)。每次送货都要从站点出发到目的地再返回,所以去A市一趟的总路程是\(2p\),去B市则是\(2(x-p)\)。现在的问题是怎么分配这些货车到各个站点,才能让所有货车一天的总路程最短?

刚开始看这个问题的时候,一眼看出:这应该是个贪心问题吧?毕竟要让总路程最小,总得让每辆车尽可能少跑路。但具体怎么贪,这里头好像有点门道。比如说,假设某辆车经常往A市跑(\(a_i\)很大),那是不是应该把它分配到离A市近的站点(也就是p小的位置)?反过来如果经常跑B市,就分配到靠近B市的站点?

不过这里有个陷阱。比如说某个站点虽然离A市近,但可能已经被很多货车占用了,剩下的容量不够。这时候就得考虑其他站点了。所以问题就变成了:如何在满足站点容量限制的前提下,给每辆货车安排最优的站点?

想到一个思路,应该把货车分成三类。第一类是往A市跑得多的(\(a_i > b_i\)),这类车要尽可能塞到p小的站点;第二类是往B市跑得多的(\(a_i < b_i\)),这类车应该往p大的站点塞;第三类是两边跑的次数一样的(\(a_i = b_i\)),这时候随便放哪都一样,因为不管选哪个p,总路程都是\(2a_i*x\)(因为\(2a*p + 2a*(x-p) = 2a*x\))。

接下来要解决的关键问题是:怎么处理前两类车的分配顺序?比如说,假设有两个站点\(p1 < p2\),现在有一辆\(a_i=100,b_i=1\)的货车和另一辆\(a_j=50,b_j=2\)的货车,应该优先把哪辆安排到p1站点?

这时候需要计算每辆车在不同站点的收益差。对于\(a_i > b_i\)的车,如果从p2换到p1,每趟A市运输能省\(2(p2 - p1)\),而B市运输会多花\(2(p2 - p1)\)。总节省量就是\(2(a_i - b_i)(p2 - p1)\)。这说明差值\((a_i - b_i)\)越大的车,放在更小的p站点能省更多路程。所以应该按\((a_i - b_i)\)从大到小排序,优先处理差值大的车。

同理,对于\(a_i < b_i\)的车,差值\((b_i - a_i)\)越大的车,放在更大的p站点收益越高。所以这类车应该按\((b_i - a_i)\)从大到小排序处理。

那具体怎么操作呢?思路大概是:

  1. 先把所有站点按p从小到大排序,这样左边是靠近A市的,右边是靠近B市的。

  2. \(a_i > b_i\)的车按\((a_i - b_i)\)降序排列,用双指针法,从最小的p开始塞,直到塞满容量。

  3. \(a_i < b_i\)的车按\((b_i - a_i)\)降序排列,从最大的p开始塞。

  4. 剩下的车(\(a_i = b_i\))随便塞到还有容量的站点,反正它们的总路程固定为\(2a_i*x\)

不过这里有个细节需要注意:处理\(a_i > b_i\)的车时,如果某个站点的容量用完了,就要移动到下一个更大的p站点。这可能会导致后面的车被分配到次优的位置。但因为我们是按差值从大到小处理的,确保最大的收益优先被锁定,这样整体最优。

举个栗子,假设x=10,有三个站点p=1(容量1)、p=2(容量1)、p=8(容量3)。有四辆货车:

1号车:a=5,b=3 → a > b,差值2
2号车:a=7,b=2 → 差值5
3号车:a=9,b=0 → 差值9
4号车:a=1,b=10000 → 显然该分配到p大的站点

这时候处理\(a_i > b_i\)的车时,先处理差值最大的3号车(差值9),分配最小的p=1站点,用掉容量。然后是2号车(差值5)分配到p=2,最后是1号车(差值2)只能分配到p=8(因为前两个站点容量用完了)。虽然1号车被分配到离A市远的站点,但这是容量限制下的最优解。

对于那辆a=1,b=10000的车,显然应该分配到最大的p=8,因为每次跑B市能省很多路程。计算一下:如果分配到p=8,每次B市运输路程是2(10-8)=4,而如果分配到p=2的话就是2(10-2)=16,每个b_i次运输就能省下12*10000=120000的里程,这差别太大了!

所以整个过程就像是在玩资源分配游戏:把最需要某种资源(小p或大p)的车优先满足,剩下的车就只能将就了。这种贪心策略虽然不能保证绝对最优(比如可能存在某些特殊排列),但在大规模数据下应该是可行的,特别是题目里给的n和m都可能到1e5,必须用O(n log n)的算法。

最后算总路程的时候,记得类型C的车(\(a_i = b_i\))直接加\(2a_i*x\)就行,不用纠结站点选择。

代码

#include <bits/stdc++.h>
using namespace std;
typedef long long ll;

struct P {
	int p, c;
	bool operator<(P o) {
		return p < o.p;
	}
};

struct T {
	int a, b;
	bool operator<(T o) {
		return a - b > o.a - o.b;
	}
};

P s[100005];
T t[100005];
int n, m, x, h, k;
ll r;

int main() {
	scanf("%d%d%d", &n, &m, &x);
	for (int i = 1; i <= n; i++)
		scanf("%d%d", &s[i].p, &s[i].c);
	for (int i = 1; i <= m; i++)
		scanf("%d%d", &t[i].a, &t[i].b);

	sort(s + 1, s + n + 1);
	sort(t + 1, t + m + 1);

	for (int i = 1; i <= m; i++)
		r += 2LL * t[i].b * x;

	h = 1, k = n;
	for (int i = 1; i <= m && t[i].a > t[i].b; i++) {
		r += 2LL * (t[i].a - t[i].b) * s[h].p;
		if (--s[h].c == 0)
			h++;
	}
	for (int i = m; i >= 1 && t[i].a < t[i].b; i--) {
		r += 2LL * (t[i].a - t[i].b) * s[k].p;
		if (--s[k].c == 0)
			k--;
	}

	printf("%lld", r);
	return 0;
}

总结

有些题对 DP 可能比较复杂,但转而想想别的思路反而能有所启发。

P10109 [GESP202312 六级] 工作沟通

题面 洛谷链接可查

P10109 [GESP202312 六级] 工作沟通

题目描述

某公司有 \(N\) 名员工,编号从 \(0\)\(N-1\)。其中,除了 \(0\) 号员工是老板,其余每名员工都有一个直接领导。我们假设编号为 \(i\) 的员工的直接领导是 \(f_i\)

该公司有严格的管理制度,每位员工只能受到本人或直接领导或间接领导的管理。具体来说,规定员工 \(x\) 可以管理员工 \(y\),当且仅当 \(x=y\),或 \(x=f_y\),或 \(x\) 可以管理 \(f_y\)。特别地,\(0\) 号员工老板只能自我管理,无法由其他任何员> 工管理。

现在,有一些同事要开展合作,他们希望找到一位同事来主持这场合作,这位同事必须能够管理参与合作的所有同事。如果有多名满足这一条件的员工,他们希望找到编号最大的员工。你能帮帮他们吗?

输入格式

第一行一个整数 \(N\),表示员工的数量。

第二行 \(N - 1\) 个用空格隔开的正整数,依次为 \(f_1,f_2,\dots f_{N−1}\)

第三行一个整数 \(Q\),表示共有 \(Q\) 场合作需要安排。

接下来 \(Q\) 行,每行描述一场合作:开头是一个整数 \(m\)\(2 \le m \le N\)),表示参与本次合作的员工数量;接着是 \(m\) 个整数,依次表示参与本次合作的员工编号(保证编号合法且不重复)。

保证公司结构合法,即不存在任意一名员工,其本人是自己的直接或间接领导。

输出格式

输出 \(Q\) 行,每行一个整数,依次为每场合作的主持人选。

输入输出样例 #1
输入 #1

5
0 0 2 2
3
2 3 4
3 2 3 4
2 1 4

输出 #1

2
2
0

输入输出样例 #2
输入 #2

7
0 1 0 2 1 2
5
2 4 6
2 4 5
3 4 5 6
4 2 4 5 6
2 3 4

输出 #2

2
1
1
1
0

说明/提示

样例解释 1

对于第一场合作,员工\(3,4\) 有共同领导 \(2\) ,可以主持合作。

对于第二场合作,员工 \(2\) 本人即可以管理所有参与者。

对于第三场合作,只有 \(0\) 号老板才能管理所有员工。

数据范围

对于 \(50\%\) 的测试点,保证 \(N \leq 50\)

对于所有测试点,保证 \(3 \leq N \leq 300\)\(Q \leq 100\)

分析

好,咱们来聊聊这个工作沟通的问题。这个问题看起来有点像是公司的层级管理,但仔细一想,其实可以把它想象成一棵树,老板是树根,其他员工是树的分支。每次合作要找主持人,其实就是找这组员工的最近公共祖先(LCA)——不过这里有个条件,得选编号最大的那个。

比如说,假设公司结构是这样的:老板0,员工1和2直接汇报给0,员工3汇报给2,员工4也汇报给2。这时候如果合作的人是3和4,他们的共同上级是2,所以主持人必须是2。但问题来了,怎么高效地找到这个最大的共同上级呢?

思路的关键点:

  1. 预处理每个员工的“领导链”:每个员工从自己开始,往上一直走到老板,记录这条路径上的所有领导。比如员工3的领导链是3→2→0,员工4是4→2→0。这样,每个员工的领导链就是他所有可能的上级。

  2. 找交集:对于每次合作,把参与员工的领导链全部拿出来,找它们的公共部分。比如合作是3和4,他们的领导链交集是2和0,这时候选最大的2。

  3. 特殊情况处理:比如当合作的员工里有老板自己,或者所有员工的共同上级只有老板,这时候就直接返回0。

举个具体的例子:假设公司结构更复杂一点,比如员工5的领导链是5→1→0,员工6的领导链是6→2→1→0。这时候如果合作的是5和6,他们的领导链交集是1→0,这时候选最大的1。

怎么实现这个思路?

  • 第一步,建链:对每个员工,从自己开始往上跳,直到老板。可以用循环或者递归来实现。比如员工i的直接领导是f[i-1](注意输入的f数组对应的是员工1到N-1的领导),所以不断往上跳,直到遇到0为止。

  • 第二步,处理查询:比如查询中的员工列表是[3,4],就把3的领导链(比如[3,2,0])和4的领导链(比如[4,2,0])取交集,得到{2,0},然后取最大的2。

可能遇到的坑:

  • 输入的索引问题:比如员工i对应的直接领导是f[i-1],因为输入的f数组从员工1开始。比如员工1的领导是f[0],员工2是f[1],以此类推。这一步如果搞错,整个领导链都会错。

  • 老板的处理:老板的领导链只有自己,所以在建链的时候遇到0就要停止。

时间复杂度分析:

  • 预处理每个员工的链需要O(N*H),H是树的高度,最坏情况下是O(N^2),但N只有300,完全没问题。

  • 每次查询需要遍历所有参与员工的链,取交集。假设每次查询有m个员工,每个链平均长度H,那么每次查询的时间是O(mH),Q次查询就是O(Qm*H)。对于题目给的限制(N=300,Q=100),这完全足够。

验证例子:

看样例输入1中的第三个查询,员工1和4。员工1的链是1→0,员工4的链是4→2→0。它们的交集只有0,所以选0。这说明当共同上级只能是老板时,能正确处理。

再比如样例输入2的第一个查询,员工4和6。他们的链分别是4→2→1→0和6→2→1→0。交集是2→1→0,选最大的2。但样例输出是2,这和预期一致。

代码

#include <bits/stdc++.h>
using namespace std;
typedef long long ll;

struct P {
	int p, c;
	bool operator<(P o) {
		return p < o.p;
	}
};

struct T {
	int a, b;
	bool operator<(T o) {
		return a - b > o.a - o.b;
	}
};

P s[100005];
T t[100005];
int n, m, x, h, k;
ll r;

int main() {
	scanf("%d%d%d", &n, &m, &x);
	for (int i = 1; i <= n; i++)
		scanf("%d%d", &s[i].p, &s[i].c);
	for (int i = 1; i <= m; i++)
		scanf("%d%d", &t[i].a, &t[i].b);

	sort(s + 1, s + n + 1);
	sort(t + 1, t + m + 1);

	for (int i = 1; i <= m; i++)
		r += 2LL * t[i].b * x;

	h = 1, k = n;
	for (int i = 1; i <= m && t[i].a > t[i].b; i++) {
		r += 2LL * (t[i].a - t[i].b) * s[h].p;
		if (--s[h].c == 0)
			h++;
	}
	for (int i = m; i >= 1 && t[i].a < t[i].b; i--) {
		r += 2LL * (t[i].a - t[i].b) * s[k].p;
		if (--s[k].c == 0)
			k--;
	}

	printf("%lld", r);
	return 0;
}

总结

这题的思路其实挺直接的,就是利用树结构的层级关系,预处理路径,再通过集合操作找共同上级。关键点在于正确构建每个员工的领导链,以及高效处理查询的交集。

P10262 [GESP样题 六级] 亲朋数

题面 洛谷链接可查

P10262 [GESP样题 六级] 亲朋数

题目描述

给定一串长度为 \(L\)、由数字 \(0\sim 9\) 组成的数字串 \(S\)。容易知道,它的连续子串儿共有 \(\frac{L(L + 1)}2\) 个。如果某个子串对应的数(允许有前导零)是 \(p\) 的> 倍数,则称该子串为数字串 \(S\) 对于 \(p\) 的亲朋数。

例如,数字串 \(S\) 为“ \(12342\) ”、\(p\)\(2\),则在 \(15\) 个连续子串中,亲朋数有“ \(12\) ”、“ \(1234\) ”、“ \(12342\) ”、“ \(2\) ”、“ \(234\) ”、“ \(2342\) ”、“ \(34\) ”、“ \(342\) ”、“ \(4\) ”、“ \(42\) ”、“ \(2\) ”等共 \(11\) 个。注意其中“ \(2\) ”出现了 \(2\) 次,但由于其在 \(S\) 中的位置不同,记为不同的亲朋数。

现在,告诉你数字串 \(S\) 和正整数 \(p\) ,你能计算出有多少个亲朋数吗?

输入格式

输入的第一行,包含一个正整数 \(p\)。约定 \(2 \leq p \leq 128\)
输入的第二行,包含一个长为 \(L\) 的数字串 \(S\)。约定 \(1 \leq L \leq 10^6\)

输出格式

输出一行一个整数表示答案。

输入输出样例 #1
输入 #1

2
102

输出 #1

5

输入输出样例 #2
输入 #2

2
12342

输出 #2

11

说明/提示
样例 1 解释

\(5\) 个亲朋数,分别 \(10\)\(102\)\(0\)\(02\)\(2\)

分析

问题大致是说,给定一个数字串和一个正整数p,求所有连续子串中是p倍数的个数。比如数字串是"12342",p是2的话,符合条件的子串有11个。直接暴力枚举所有子串显然行不通,因为当数字串长度达到1e6时,时间复杂度直接爆炸。那怎么办呢?我们不妨换个思路想。


暴力

首先,最直观的暴力法:遍历所有可能的子串,转换成数字,判断是否是p的倍数。比如对于长度为L的字符串,子串数量是L*(L+1)/2,当L=1e6时,这大约是5e11次计算,完全不可能完成。这时候必须找数学规律或动态规划的技巧。


取模

问题的核心在于如何快速判断一个子串对应的数是否是p的倍数。直接计算数值会溢出,但利用模运算的性质可以巧妙绕过这个问题。这里的关键是:如果某个数对p取模的结果是0,那么它就是p的倍数

假设我们有一个子串S[i...j],它对应的数值可以表示为:

S[i] * 10^(j-i) + S[i+1] * 10^(j-i-1) + ... + S[j]

直接计算这个数显然不现实。但如果我们能逐步计算余数,就能避免大数运算。比如,从右到左或从左到右遍历时,每一步的余数可以通过前一步的结果推导出来。


动归

这里需要引入动态规划的思想。我们维护一个数组pre,记录以当前位置的前一个字符结尾的所有子串的余数分布。比如,pre[r]表示以某个字符结尾的子串中,余数为r的子串数量。

当处理当前字符时,假设当前字符是d,我们需要更新余数。具体来说:

  1. 单独以当前字符d结尾的子串:余数是d % p
  2. 以当前字符d结尾的更长子串:假设前一个位置的某个余数是r,那么新的余数是(r * 10 + d) % p

举个例子,假设p=2,当前字符是2,前一个位置的余数分布是pre[0]=3pre[1]=1。那么:

  • 对于余数0的子串,新余数是(0 * 10 + 2) % 2 = 2 % 2 = 0,所以当前余数0的数量会增加3。
  • 对于余数1的子串,新余数是(1 * 10 + 2) % 2 = 12 % 2 = 0,所以余数0的数量再增加1。
  • 再加上当前字符单独形成的子串(余数0),总共有3 + 1 + 1 = 5个余数0的子串。

样例

以样例1为例,输入是p=2,数字串"102":

  1. 处理第一个字符'1'
    • 单独子串"1",余数1%2=1。
    • pre = {1:1},此时余数0的数量为0。
  2. 处理第二个字符'0'
    • 单独子串"0",余数0%2=0。
    • 前一个余数1的子串(即"1")扩展为"10",余数(1*10+0)%2=0。
    • 当前余数0的总数是1(单独"0") + 1("10")=2。
    • pre = {0:2},总答案累计2。
  3. 处理第三个字符'2'
    • 单独子串"2",余数2%2=0。
    • 前一个余数0的子串("0"和"10")扩展为"02"和"102",余数(0*10+2)%2=0。
    • 当前余数0的总数是1(单独"2") + 2(扩展)=3。
    • 总答案累计2+3=5,与样例输出一致。

优化

注意到每次更新余数时,只需要前一步的结果。因此可以用两个数组交替保存当前和上一步的余数状态,避免重复分配内存。比如:

  • 初始时,pre数组记录初始状态。
  • 处理每个字符时,用curr数组记录新的余数分布,然后交换precurr

边界
  • 前导零的处理:题目允许子串有前导零,因此像"02"这样的子串需要被统计。
  • 余数为0的初始状态:比如数字串以0开头时,单独一个0就是一个有效子串。

代码

#include<bits/stdc++.h>
using namespace std;
char s[1000005];
int p,a[129],b[129],*c=b,*p1=a;
long long ans;
int main(){
    scanf("%d%s",&p,s);
    for(int i=0;s[i];i++){
        int d=s[i]-'0',t;
        memset(c,0,p*4);
        c[d%p]++;
        for(int j=0;j<p;j++)if(t=p1[j])c[(j*10+d)%p]+=t;
        ans+=c[0];
        int *tmp=p1;p1=c;c=tmp;
    }
    printf("%lld",ans);
}

总结

这个问题的核心是通过动态规划维护余数分布,逐步累加符合条件的子串数量。时间复杂度从暴力法的O(L²)优化到O(L*p),其中p最大为128,完全可以在合理时间内处理1e6长度的字符串。这种思路在类似“子串统计”问题中非常常见,比如求子串和为k的倍数等问题,都可以用类似的方法优化。

P10250 [GESP样题 六级] 下楼梯

题面 洛谷链接可查

P10250 [GESP样题 六级] 下楼梯

题目描述

顽皮的小明发现,下楼梯时每步可以走 \(1\) 个台阶、\(2\) 个台阶或 \(3\) 个台阶。现在一共有 \(N\) 个台阶,你能帮小明算算有多少种方案吗?

输入格式

输入一行,包含一个整数 \(N\)

输出格式

输出一行一个整数表示答案。

输入输出样例 #1
输入 #1

4

输出 #1

7

输入输出样例 #2
输入 #2

10

输出 #2

174

说明/提示

对全部的测试点,保证 \(1 \leq N \leq 60\)

分析

这道题也是算比较简单了,很快就能想到一个思路:当前楼梯一步到这里的可能就三种 分别就是后退的前三种

那么同理到这个楼梯的可能就是前三种楼梯的可能数相加。递推式即:

$ dp_i = dp_{i-1} + dp_{i-2} + dp_{i-3} $

就这样即可完成本题。

吗?如果你真的写完代码,注意,不开long long见祖宗。

代码

#include <bits/stdc++.h>
using namespace std;
long long n, dp[100010];

int main() {
	cin >> n;
	dp[1] = 1;
	dp[2] = 2;
	dp[3] = 4;
	for (int i = 4; i <= n; i++)
		dp[i] = dp[i - 1] + dp[i - 2] + dp[i - 3];
	cout << dp[n];
	return 0;
}

总结

典型,好题,有坑。

P10724 [GESP202406 七级] 区间乘积

题面 洛谷链接可查

P10724 [GESP202406 七级] 区间乘积

题目描述

小杨有一个包含 \(n\) 个正整数的序列 \(A=[a_1,a_2,\ldots,a_n]\)

小杨想知道有多少对 \(\langle l,r\rangle(1\leq l\leq r\leq n)\) 满足 \(a_l\times a_{l+1}\times\ldots\times a_r\) 为完全平> > 方数。

一个正整数 \(x\) 为完全平方数当且仅当存在一个正整数 \(y\) 使得 \(x=y\times y\)

输入格式

第一行包含一个正整数 \(n\),代表正整数个数。

第二行包含 \(n\) 个正整数 \(a_i\),代表序列 \(A\)

输出格式

输出一个整数,代表满足要求的 \(\langle l,r\rangle\) 数量。

输入输出样例 #1
输入 #1

5
3 2 4 3 2

输出 #1

2

说明/提示
样例解释

满足条件的 \(\langle l,r\rangle\)\(\langle 1,5\rangle\)\(\langle 3,3\rangle\)

数据范围
子任务编号 数据点占比 \(n\) \(a_i\)
\(1\) \(20\%\) \(\leq 10^5\) \(1\leq a_i\leq 2\)
\(2\) \(40\%\) \(\leq 100\) \(1\leq a_i\leq 30\)
\(3\) \(40\%\) \(\leq 10^5\) \(1\leq a_i\leq 30\)

分析

哎,这题看起来简单,上来就想暴力枚举所有区间——然后看到数据规模直接傻眼。1e5的n,可以确定的是暴力算O(n²)肯定超时。

突然想到完全平方数的本质。乘积要是平方数,那每个质因数的指数都得是偶数对吧?这时候就开始翻抽屉里的数学笔记——对,分解质因数!但怎么快速判断这么多区间的指数奇偶性呢?

把每个数的质因数分解结果,用二进制表示奇偶性?比如质数2的指数是奇数就标1,偶数标0。这样每个数都能转化成一个二进制掩码。

但问题来了:怎么快速判断区间的异或结果?这时候可以用到前缀和的概念。不过普通的加法前缀和不行啊,得用异或前缀和!因为异或的特性正好能反映奇偶性的叠加效果。

然后遇到个坑:质数范围到底怎么定?题目说a_i最大30,那直接枚举30以内的质数就行。2、3、5...列出来发现刚好10个,用int的每一位刚好能存下。

最后用哈希表记录前缀异或值的出现次数。每次看到重复的异或值就说明中间这段区间的乘积是平方数。最后把O(n²)的问题压成了O(n)的时间复杂度。

综上所述,我们就可以解决这道题。

代码

#include <bits/stdc++.h>
using namespace std;
typedef long long ll;

int main() {
	int n;
	cin >> n;
	vector<int> a(n);
	for (int i = 0; i < n; ++i) {
		cin >> a[i];
	}

	int p[] = {2, 3, 5, 7, 11, 13, 17, 19, 23, 29};
	int np = sizeof(p) / sizeof(p[0]);
	map<int, int> p2b;
	for (int i = 0; i < np; ++i) {
		p2b[p[i]] = i;
	}

	map<int, int> cnt;
	cnt[0] = 1;
	int s = 0;
	ll ans = 0;

	for (int i = 0; i < n; ++i) {
		int x = a[i];
		int m = 0;
		for (int j = 0; j < np; ++j) {
			int pr = p[j];
			if (x == 1)
				break;
			int e = 0;
			while (x % pr == 0) {
				e++;
				x /= pr;
			}
			if (e % 2 == 1) {
				m ^= (1 << p2b[pr]);
			}
		}
		s ^= m;
		ans += cnt[s];
		cnt[s]++;
	}

	cout << ans << endl;

	return 0;
}

总结

这种题不是特别的困难,重点在于如何变换思路,总而言之,这道题可能只有上位黄的水平,放在绿题是比较小题大做了。

P10265 [GESP样题 七级] 迷宫统计

题面 洛谷链接可查

P10265 [GESP样题 七级] 迷宫统计

题目描述

在神秘的幻想⼤陆中,存在着 \(n\) 个古老而神奇的迷宫,迷宫编号从 \(1\)\(n\)。有的迷宫之间可以直接往返,有的可以⾛到别的迷宫,但是不能⾛回来。玩家小杨想挑战⼀下不同的迷宫,他决定从 \(m\) 号迷宫出发。现在,他需要你帮助他统计:有多少迷宫可以直接到达 \(m\) 号迷宫,\(m\) 号迷宫可以直接到达其他的迷宫有多少,并求出他们的和。

需要注意的是,对于 \(i\) (\(1 \leq i \leq n\)) 号迷宫,它总可以直接到达自身。

输入格式

第一行两个整数 \(n\)\(m\),分别表示结点迷宫总数,指定出发迷宫的编号。
下面 \(n\) 行,每行 \(n\) 个整数,表示迷宫之间的关系。对于第 \(i\) 行第 \(j\) 列的整数,\(1\) 表示能从 \(i\) 号迷宫直接到达 \(j\) 号迷宫,\(0\) 表示不能直接到达。

输出格式

一行输出空格分隔的三个整数,分别表示迷宫 \(m\) 可以直接到达其他的迷宫有多少个,有多少迷宫可以直接到达 \(m\) 号迷宫,这些迷宫的总和。

输入输出样例 #1
输入 #1
6 4
1 1 0 1 0 0
0 1 1 0 0 0
1 0 1 0 0 1
0 0 1 1 0 1
0 0 0 1 1 0
1 0 0 0 1 1
输出 #1
3 3 6
说明/提示

样例 1 解释

\(4\) 号迷宫能直接到达的迷宫有 \(3,4,6\) 号迷宫,共 \(3\) 个。
能直接到达 \(4\) 号迷宫的迷宫有 \(1,4,5\) 号迷宫,共 \(3\) 个。

共 6 个。

数据规模与约定
子任务 分值 $n \leq $
\(1\) \(30\) \(10\)
\(2\) \(30\) \(100\)
\(3\) \(40\) \(1000\)

对全部的测试数据,保证 \(1 \leq m \leq n \leq 1000\)

分析

很容易发现就是一个邻接矩阵,从两边分别统计一下入度和出度的数量就可以了。

代码

#include<bits/stdc++.h>
using namespace std;
const int MAXN=1e3+10;
int main(){
	int ans1=0,ans2=0;
	int n,m,arr[MAXN][MAXN];
	cin>>n>>m;
	for(int i=1;i<=n;i++)
		for(int j=1;j<=n;j++)
			cin>>arr[i][j];
	for(int i=1;i<=n;i++){
		if(arr[i][m]) ans1++;
		if(arr[m][i]) ans2++;
	}
	cout<<ans1<<" "<<ans2<<" "<<ans1+ans2;	
	return 0;
}

总结

P10721 [GESP202406 六级] 计算得分

题面 洛谷链接可查

P10721 [GESP202406 六级] 计算得分

题目描述

小杨想要计算由 \(m\) 个小写字母组成的字符串的得分。

小杨设置了一个包含 \(n\) 个正整数的计分序列 \(A=[a_1,a_2,\ldots,a_n]\),如果字符串的一个子串由 \(k(1\leq k \leq n)\)\(\texttt{abc}\) 首尾相接组成,那么能够得到分数 \(a_k\),并且字符串包含的字符不能够重复计算得分,整个字符串的得分是计分子串的总和。

例如,假设 ,字符串 \(\texttt{dabcabcabcabzabc}\) 的所有可能计分方式如下:

  • \(\texttt{d+abc+abcabc+abz+abc}\) 或者 \(\texttt{d+abcabc+abc+abz+abc}\),其中 \(\texttt{d}\)\(\texttt{abz}\) 不计算得分,总得分为 \(a_1+a_2+a_1\)
  • \(\texttt{d+abc+abc+abc+abz+abc}\),总得分为 \(a_1+a_1+a_1+a_1\)
  • \(\texttt{d+abcabcabc+abz+abc}\),总得分为 \(a_3+a_1\)

小杨想知道对于给定的字符串,最大总得分是多少。

输入格式
  • 第一行包含一个正整数 \(n\),代表计分序列 \(A\) 的长度。

  • 第二行包含 \(n\) 个正整数,代表计分序列 \(A\)

  • 第三行包含一个正整数 \(m\),代表字符串的长度。

  • 第四行包含一个由 \(m\) 个小写字母组成的字符串。

输出格式

输出一个整数,代表给定字符串的最大总得分。

输入输出样例 #1
输入 #1
3
3 1 2
13
dabcabcabcabz
输出 #1
9
说明/提示
样例解释

最优的计分方式为 \(\texttt{d+abc+abc+abc+abz}\),总得分为 \(a_1+a_1+a_1\),共 \(9\) 分。

数据范围
子任务编号 数据点占比 \(n\) \(m\) \(a_i\) 特殊性质
\(1\) \(20\%\) \(20\) \(10^5\) \(1000\)
\(2\) \(40\%\) \(3\) \(10^5\) \(1000\)
\(3\) \(40\%\) \(20\) \(10^5\) \(1000\)

对于全部数据,保证有 \(1\leq n\leq 20\)\(1\leq m\leq 10^5\)\(1\leq a_i\leq 1000\)

分析

设置一个状态 \(s_i\) 表示以当前这个位置向前取能找到的 \(abc\) 数量。

那么:

\(如果 i−2→i 该段是 abc 串,则 s_i 的数值可以由 s_{i−3}+1 转移。\)

对于一个 \(s_i\) 要么选择上一个状态,要么选择前一个 \(abc\) 串状态。

综上所述,得:\(dp_i=max(dp_{i-1},dp_{i-3 × j}+a_j)\)

代码

#include <bits/stdc++.h>
using namespace std;
const int N = 25;
const int M = 1e5 + 10;
int n, m;
int a[N];
string s;
int can[M];
int dp[M];

int main() {
	scanf("%d", &n);
	for (int i = 1; i <= n; i++)
		scanf("%d", a + i);
	scanf("%d", &m);
	cin >> s;
	for (int i = 2; i < m; i++) {
		if (s[i] == 'c') {
			if (s[i - 1] == 'b' && s[i - 2] == 'a') {
				can[i] = can[i - 3] + 1;
				if(s[i-1]=='c')
					continue;
				if(s[i-1]==s[i])
					break;
			}
		}
	}
	for (int i = 0; i < m; i++) {
		dp[i] = dp[i - 1];
		for (int j = 1; j <= min(can[i], n); j++) {
			dp[i] = max(dp[i], dp[i - 3 * j] + a[j]);
		}
	}
	printf("%d\n", dp[m - 1]);
	return 0;
}

总结

变换思路的动态规划,很适合练习。

posted @ 2025-04-05 11:40  Easoncalm  阅读(125)  评论(0)    收藏  举报