这里是KK爱Coding 团队的paoxiaomo,一个现役ACMer,之后将会持续更新算法笔记系列以及笔试题题解系列
感谢大家的订阅➕ 和 喜欢💗
01.硬币最少组合问题
问题描述
K小姐有一个爱好就是收集各种硬币。有一天,她在整理自己的硬币收藏时,突然想到了一个有趣的问题。如果给定一个总金额 amount,能够用手上现有的硬币面值组合成该金额,需要的硬币数量最少是多少枚呢?
现在已知K小姐有 5 种不同面值的硬币,分别为 1、2、5、10、20、50、100 元,每种面值的硬币数量都是无限的。请你帮助K小姐设计一个程序,快速计算出给定总金额所需的最少硬币组合。
输入格式
输入仅包含一个正整数 amount,表示要求组合的目标总金额,单位为元。
输出格式
输出最少硬币组合,从大到小按面值顺序输出,硬币面值间用空格分隔。若无法用现有面值组合出目标总金额,则输出 -1。
样例输入
8
样例输出
5 2 1
数据范围
1 \leq amount \leq 10^5
题解
这是一个典型的动态规划问题。可以定义一个一维数组 dp,其中 dp[i] 表示组合成金额 i 所需的最少硬币数量。
先将 dp 数组初始化为一个较大的值,表示暂时无法组合出该金额。然后对于每一种面值的硬币,遍历 amount,尝试更新 dp[j] 的值。具体来说,对于面值为 val 的硬币,可以将金额 j 拆分为 val 和 j-val 两部分,即 dp[j] = dp[j-val] + 1,表示组合成金额 j 的最少硬币数等于组合成金额 j-val 的最少硬币数再加上当前这枚面值为 val 的硬币。
最后,再从 dp[amount] 开始回溯,找出凑成总金额的硬币组合方案。具体做法是,从 amount 开始,如果 dp[j] = dp[last] - 1,即找到了上一个状态,就将两个状态的差值 last - j 加入答案数组中,然后令 last = j,直到 j 减小到 0 为止。因为要求输出的硬币面值要从大到小排列,所以最后再对答案数组逆序输出即可。
参考代码
-
Python
import sys
n = int(sys.stdin.readline().strip())
coins = [1, 2, 5, 10, 20, 50, 100]
INF = float('inf')
dp = [INF] * (n + 1)
dp[0] = 0
for val in coins:
for j in range(val, n + 1):
dp[j] = min(dp[j], dp[j - val] + 1)
if dp[n] == INF or n == 0:
print(-1)
exit(0)
result = []
last, j = n, n
while j >= 0:
if dp[j] == dp[last] - 1:
result.append(last - j)
last = j
j -= 1
print(*reversed(result))
-
Java
import java.util.*;
public class Main {
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
int n = sc.nextInt();
int[] coins = {1, 2, 5, 10, 20, 50, 100};
int INF = Integer.MAX_VALUE;
int[] dp = new int[n + 1];
Arrays.fill(dp, INF);
dp[0] = 0;
for (int val : coins) {
for (int j = val; j <= n; j++) {
dp[j] = Math.min(dp[j], dp[j - val] + 1);
}
}
if (dp[n] == INF || n == 0) {
System.out.println(-1);
return;
}
List<Integer> result = new ArrayList<>();
int last = n, j = n;
while (j >= 0) {
if (dp[j] == dp[last] - 1) {
result.add(last - j);
last = j;
}
j--;
}
Collections.reverse(result);
for (int num : result) {
System.out.print(num + " ");
}
}
}
-
Cpp
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
const int INF = 0x3f3f3f3f;
int main() {
int n;
cin >> n;
vector<int> coins = {1, 2, 5, 10, 20, 50, 100};
vector<int> dp(n + 1, INF);
dp[0] = 0;
for (int val : coins) {
for (int j = val; j <= n; j++) {
dp[j] = min(dp[j], dp[j - val] + 1);
}
}
if (dp[n] == INF || n == 0) {
cout << -1 << endl;
return 0;
}
vector<int> result;
int last = n, j = n;
while (j >= 0) {
if (dp[j] == dp[last] - 1) {
result.push_back(last - j);
last = j;
}
j--;
}
reverse(result.begin(), result.end());
for (int num : result) {
cout << num << " ";
}
return 0;
}
02.单词拆分问题
问题描述
LYA最近在学习一门新的外语,他发现有些外来词在词典中并没有收录。为了方便学习和记忆,他想将这些生词拆分成词典中已有的单词。
现在给定一个词典,其中每个单词都有一个正整数权重。对于一个待拆分的单词,可以将其拆分成词典中出现的单词,且拆分得到的单词序列的权重之和最大。
例如,词典中有 i、flytek、inly 三个单词,权重分别为 3、1、5。对于单词 ilytek,可以拆分成 i/flytek 或 ily/tek,对应的权重之和分别为 4 和 8,因此应该拆分为 ily/tek。
假设词典中最多有 100 个单词,每个单词的长度不超过 50 个字符。请你帮助LYA设计一个程序,能够根据词典将输入的单词拆分成权重之和最大的形式。
输入格式
第一行包含整数 N,表示词典中单词的个数。
接下来 N 行,每行包含一个单词和对应的权重值,以空格分隔。
最后一行输入一个待拆分的单词。
输出格式
如果可以将输入的单词拆分,则输出拆分后的结果,单词之间用 / 隔开。如果无法拆分,则原样输出待拆分的单词。
样例输入
7
ba 1
cef 2
cefs 3
s 2
dok 6
sdok 9
ok 3
bacefsdok
样例输出
/ba/cef/sdok
数据范围
1 \leq N \leq 100 1 \leq 单词长度 \leq 50
题解
本题可以使用动态规划来解决。设 dp[i] 表示将前 i 个字符拆分得到的最大权重和,fore[i] 表示前 i 个字符拆分得到最大权重和时,最后一个单词的前一个字符的下标。
我们可以从前往后枚举每个字符作为当前拆分的结束位置,然后枚举上一个单词的结束位置,判断中间这一段是否能在词典中找到。如果能找到,就更新 dp[i] 和 fore[i]。
最后,如果 dp[n] 的值没有被更新过,说明无法拆分,原样输出单词。否则,从 fore[n] 开始,依次向前跳转,就能得到拆分的单词序列。
第二种思路,使用前缀和来
参考代码
-
Python
def main():
n = int(input())
words = {}
for _ in range(n):
word, weight = input().split()
words[word] = int(weight)
s = input()
m = len(s)
dp = [-float('inf')] * (m + 1)
fore = [-1] * (m + 1)
dp[0] = 0
for i in range(1, m + 1):
for j in range(i):
word = s[j:i]
if word in words and dp[j] + words[word] > dp[i]:
dp[i] = dp[j] + words[word]
fore[i] = j
if dp[m] == -float('inf'):
print(s)
else:
res = []
i = m
while i > 0:
res.append(s[fore[i]:i])
i = fore[i]
print('/' + '/'.join(res[::-1]))
if __name__ == "__main__":
main()
-
Java
import java.io.*;
import java.util.*;
public class Main {
public static void main(String[] args) throws IOException {
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
int n = Integer.parseInt(br.readLine());
Map<String, Integer> words = new HashMap<>();
for (int i = 0; i < n; i++) {
String[] input = br.readLine().split(" ");
words.put(input[0], Integer.parseInt(input[1]));
}
String s = br.readLine();
int m = s.length();
int[] dp = new int[m + 1];
Arrays.fill(dp, Integer.MIN_VALUE);
int[] fore = new int[m + 1];
dp[0] = 0;
for (int i = 1; i <= m; i++) {
for (int j = 0; j < i; j++) {
String word = s.substring(j, i);
if (words.containsKey(word) && dp[j] + words.get(word) > dp[i]) {
dp[i] = dp[j] + words.get(word);
fore[i] = j;
}
}
}
if (dp[m] == Integer.MIN_VALUE) {
System.out.println(s);
} else {
List<String> res = new ArrayList<>();
int i = m;
while (i > 0) {
res.add(s.substring(fore[i], i));
i = fore[i];
}
Collections.reverse(res);
System.out.println("/" + String.join("/", res));
}
}
}
-
Cpp
#include <iostream>
#include <unordered_map>
#include <vector>
#include <climits>
#include <algorithm>
using namespace std;
int main() {
int n;
cin >> n;
unordered_map<string, int> words;
for (int i = 0; i < n; i++) {
string word;
int weight;
cin >> word >> weight;
words[word] = weight;
}
string s;
cin >> s;
int m = s.length();
vector<int> dp(m + 1, INT_MIN);
vector<int> fore(m + 1);
dp[0] = 0;
for (int i = 1; i <= m; i++) {
for (int j = 0; j < i; j++) {
string word = s.substr(j, i - j);
if (words.count(word) && dp[j] + words[word] > dp[i]) {
dp[i] = dp[j] + words[word];
fore[i] = j;
}
}
}
if (dp[m] == INT_MIN) {
cout << s << endl;
} else {
vector<string> res;
int i = m;
while (i > 0) {
res.push_back(s.substr(fore[i], i - fore[i]));
i = fore[i];
}
reverse(res.begin(), res.end());
cout << "/" << res[0];
for (int i = 1; i < res.size(); i++) {
cout << "/" << res[i];
}
cout << endl;
}
return 0;
}
03.K小姐的幸运评论
题目描述
K小姐是一位著名的博主,她在社交平台上发布了一篇文章。文章发布后,粉丝们纷纷在评论区留下了自己的观点和看法。为了鼓励大家积极评论,K小姐决定从评论者中选出一位"最幸运评论者",送出神秘大奖。
评论者的选取规则如下:
- 每个评论都有一个唯一的编号,从 0 开始递增。
- 每个评论下面都有点赞和点踩功能,定义每条评论的净赞数量为该评论点赞数减去点踩数。净赞数量可以是负数。
- 在编号连续的评论段中,存在一个净赞数量之和最大的区间,记为区间 [l,r]。
- 在区间 [l,r] 内净赞数最多的评论者即为"最幸运评论者"。如果有并列,则取编号最大的评论者。
现在请你帮助K小姐设计一个程序,处理评论数据,找出这位幸运的粉丝。
输入格式
第一行包含一个整数 n,表示评论的总数。
第二行包含 n 个整数,每个整数表示一条评论的净赞数量。
输出格式
输出共 1 行,包含 5 个整数 l,r,maxv,p,v,分别表示最大净赞区间的左右端点编号、区间内净赞总量、"最幸运评论者"的编号、该评论者的净赞数量。
样例输入
10
-99 -58 -52 7 89 48 43 -10 94 69
样例输出
3 9 340 8 94
数据范围
1 \leq n \leq 10^5 -10^4 \leq 净赞数量 \leq 10^4
题解
本题是一道经典的最大子段和问题,但是在此基础上还需要找出最大子段中的最大值及其位置。
我们可以使用动态规划来解决最大子段和问题。定义 dp[i] 表示以位置 i 结尾的最大子段和,dp[i] 的状态转移方程为:
dp[i]=\begin{cases} nums[i] & ,\text{if } dp[i-1] \leq 0 \\ dp[i-1]+nums[i] &,\text{otherwise} \end{cases}
同时我们需要记录区间的左右端点、区间内的最大值及其位置。
- 当 dp[i-1] \leq 0 时,说明前面的子段和为负,舍弃前面的部分,从位置 i 开始重新计算区间。
- 当 dp[i-1]>0 时,前面的子段和为正,可以继续延伸当前区间。
- 记录最大子段和 maxSum,以及对应的左右端点 l,r。
- 用单独的变量 maxNum,pos 记录区间内的最大值和对应下标。
最后,输出 l,r,maxSum,pos,maxNum 即可。
时间复杂度 O(n),空间复杂度 O(n)。其中 n 为数组长度。
参考代码
-
Python
n = int(input())
nums = list(map(int, input().split()))
l = r = 0
maxSum = float('-inf')
dp = [0] * n
dp[0] = nums[0]
maxNum = nums[0]
pos = 0
for i in range(1, n):
if dp[i-1] > 0:
dp[i] = dp[i-1] + nums[i]
else:
dp[i] = nums[i]
l = i
if dp[i] > maxSum:
maxSum = dp[i]
r = i
if nums[i] > maxNum:
maxNum = nums[i]
pos = i
print(l, r, maxSum, pos, maxNum)
-
Java
import java.util.Scanner;
public class Main {
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
int n = sc.nextInt();
int[] nums = new int[n];
for (int i = 0; i < n; i++) {
nums[i] = sc.nextInt();
}
int l = 0, r = 0;
int maxSum = Integer.MIN_VALUE;
int[] dp = new int[n];
dp[0] = nums[0];
int maxNum = nums[0];
int pos = 0;
for (int i = 1; i < n; i++) {
if (dp[i-1] > 0) {
dp[i] = dp[i-1] + nums[i];
} else {
dp[i] = nums[i];
l = i;
}
if (dp[i] > maxSum) {
maxSum = dp[i];
r = i;
}
if (nums[i] > maxNum) {
maxNum = nums[i];
pos = i;
}
}
System.out.print(l + " " + r + " " + maxSum + " " + pos + " " + maxNum);
}
}
-
Cpp
#include <iostream>
#include <vector>
using namespace std;
int main() {
int n;
cin >> n;
vector<int> nums(n);
for (int i = 0; i < n; i++) {
cin >> nums[i];
}
int l = 0, r = 0;
int maxSum = INT_MIN;
vector<int> dp(n);
dp[0] = nums[0];
int maxNum = nums[0];
int pos = 0;
for (int i = 1; i < n; i++) {
if (dp[i-1] > 0) {
dp[i] = dp[i-1] + nums[i];
} else {
dp[i] = nums[i];
l = i;
}
if (dp[i] > maxSum) {
maxSum = dp[i];
r = i;
}
if (nums[i] > maxNum) {
maxNum = nums[i];
pos = i;
}
}
cout << l << " " << r << " " << maxSum << " " << pos << " " << maxNum << endl;
return 0;
}