作者注:本文中代码均在 C++14 (GCC 9) O2 编译环境下编译通过。
猫猫 TOM 和小老鼠 JERRY 最近又较量上了,但是毕竟都是成年人,他们已经不喜欢再玩那种你追我赶的游戏,现在他们喜欢玩统计。
最近,TOM 老猫查阅到一个人类称之为“逆序对”的东西,这东西是这样定义的:对于给定的一段正整数序列,逆序对就是序列中 a i > a j a_i>a_j ai>aj 且 i < j i
第一行,一个数 n n n,表示序列中有 n n n 个数。
第二行 n n n 个数,表示给定的序列。序列中每个数字不超过 1 0 9 10^9 109。
输出序列中逆序对的数目。
6
5 4 2 6 3 1
11
归并排序 (merge sort) 是利用归并的思想实现的排序方法,该算法采用经典的“分而治之”的策略,主要分为递归拆分子序列和合并相邻有序子序列两个步骤。归并排序是一种稳定的排序算法,其时间复杂度为 O ( n log n ) O(n\log n) O(nlogn)。
对于本题,如果我们想要将一个序列排成升序序列,那么每次拆分后再合并时,左右两个子序列都是升序的,因此只需要统计右侧的序列中的每个数分别会与左侧的序列产生多少逆序对。
另外需要注意的一点是,本题的结果可能会超出int
的范围,因此需要开long long
类型的全局变量用来存放结果。
Language: C++
#include
using namespace std;
const int N = 500005;
long long ans;
int a[N];
int t[N];
void merge_sort(int b, int e) {
if (b == e) {
return;
}
int mid = b + ((e - b) >> 1);
int i = b;
int j = mid + 1;
int k = b;
merge_sort(b, mid);
merge_sort(mid + 1, e);
while (i <= mid && j <= e) {
if (a[i] <= a[j]) {
t[k++] = a[i++];
} else {
t[k++] = a[j++];
ans += mid - i + 1;
}
}
while (i <= mid) {
t[k++] = a[i++];
}
while (j <= e) {
t[k++] = a[j++];
}
for (int m = b; m <= e; m++) {
a[m] = t[m];
}
}
int main() {
int n;
cin >> n;
for (int i = 0; i < n; i++) {
cin >> a[i];
}
merge_sort(0, n - 1);
cout << ans << endl;
return 0;
}
本题还可以用数据离散化+树状数组的解法解决。
根据值来建立树状数组,在循环到第 i i i 项时,前 i − 1 i-1 i−1 项都已加入树状数组。树状数组内比 a i a_i ai 大的元素都会与 a i a_i ai 构成逆序对,逆序对数量为 i − q u e r y ( a i ) i-query(a_i) i−query(ai),其中 q u e r y ( a i ) query(a_i) query(ai) 代表在树状数组内询问 1 1 1 到 a i a_i ai 项的前缀和。
观察数据范围,容易发现根据值来建立树状数组的空间不够,因此可以考虑对数据离散化。我们只需要数据之间的相对大小关系,因此可以将数据排序,再用区间 [ 1 , n ] [1,n] [1,n] 上的数表示原始数据的相对大小关系,最后对这个新的序列建立树状数组即可。
题目描述中最后一句说明序列中可能有重复数字,不处理相等的元素可能会导致求解过程的错误。当有与 a i a_i ai 相等的元素在 a i a_i ai 前被加入树状数组且其相对大小标记更大的时候,就会误将两个相等的数判定为逆序对。我们在排序的时候,需要将先出现的数字的标记也设置为较小的,可以考虑在输入数据时利用结构体数组存储,结构体成员变量分别为原始数据以及其对应的原始下标。在排序时可以指定sort
函数的cmp
参数,将原始数据作为第一关键字,原始下标作为第二关键字,对结构体数组升序排序。
Language: C++
#include
#include
using namespace std;
const int N = 500005;
int n;
struct node {
int idx;
int val;
} a[N];
int tree[N];
int rank_[N];
long long ans;
inline void insert(int p, int d) {
for (; p <= n; p += p & -p) {
tree[p] += d;
}
}
inline int query(int p) {
int ret = 0;
for (; p; p -= p & -p) {
ret += tree[p];
}
return ret;
}
int main() {
cin >> n;
for (int i = 1; i <= n; i++) {
cin >> a[i].val;
a[i].idx = i;
}
sort(a + 1, a + n + 1, [](const node& o1, const node& o2) {
return o1.val == o2.val ? o1.idx < o2.idx : o1.val < o2.val;
});
for (int i = 1; i <= n; i++) {
rank_[a[i].idx] = i;
}
for (int i = 1; i <= n; i++) {
insert(rank_[i], 1);
ans += i - query(rank_[i]);
}
cout << ans << endl;
return 0;
}
You have an array a a a of length n n n. For every positive integer x x x you are going to perform the following operation during the x x x-th second:
You have to make a a a nondecreasing as fast as possible. Find the smallest number T T T such that you can make the array nondecreasing after at most T T T seconds.
Array a a a is nondecreasing if and only if a 1 ≤ a 2 ≤ ⋯ ≤ a n a_1 \leq a_2 \leq \cdots \leq a_n a1≤a2≤⋯≤an.
You have to answer t t t independent test cases.
The first line contains a single integer t ( 1 ≤ t ≤ 1 0 4 ) t\ (1 \leq t \leq 10^4) t (1≤t≤104) — the number of test cases.
The first line of each test case contains single integer n ( 1 ≤ n ≤ 1 0 5 ) n\ (1 \leq n \leq 10^5) n (1≤n≤105) — the length of array a a a. It is guaranteed that the sum of values of n n n over all test cases in the input does not exceed 1 0 5 10^5 105.
The second line of each test case contains n n n integers a 1 , a 2 , ⋯ , a n ( − 1 0 9 ≤ a i ≤ 1 0 9 ) a_1,a_2,\cdots,a_n\ (−10^9 \leq a_i \leq 10^9) a1,a2,⋯,an (−109≤ai≤109).
For each test case, print the minimum number of seconds in which you can make a a a nondecreasing.
3
4
1 7 6 5
5
1 2 3 4 5
2
0 -4
2
0
3
题目大意:有一个长度为 n n n 的数组 a a a,在第 x x x 秒内可以选择介于 1 1 1 和 n n n 之间的索引 i 1 , i 2 , ⋯ , i k i_1, i_2, \cdots, i_k i1,i2,⋯,ik,然后在数组 a a a 的对应位置加上 2 x − 1 2^{x-1} 2x−1,也可以不选择任何索引。找出最短的时间 T T T,使得执行操作后数组 a a a 非严格递增。
本题采取贪心策略,找到数组中最大的降序差值,然后判断是 2 2 2 的多少倍即可。当最大的降序差值都使得数组非降序的时候,比其小的也一定实现了数组非降序。
Language: C++
#include
#include
using namespace std;
int main() {
int t;
cin >> t;
while (t--) {
int n;
cin >> n;
int a_max = INT_MIN;
int diff = 0;
for (int i = 0; i < n; i++) {
int a;
cin >> a;
a_max = max(a, a_max);
diff = max(diff, a_max - a);
}
int ans = 0;
while (diff) {
diff /= 2;
ans++;
}
cout << ans << endl;
}
return 0;
}
在一个果园里,多多已经将所有的果子打了下来,而且按果子的不同种类分成了不同的堆。多多决定把所有的果子合成一堆。
每一次合并,多多可以把两堆果子合并到一起,消耗的体力等于两堆果子的重量之和。可以看出,所有的果子经过 n − 1 n-1 n−1 次合并之后, 就只剩下一堆了。多多在合并果子时总共消耗的体力等于每次合并所耗体力之和。
因为还要花大力气把这些果子搬回家,所以多多在合并果子时要尽可能地节省体力。假定每个果子重量都为 1 1 1,并且已知果子的种类数和每种果子的数目,你的任务是设计出合并的次序方案,使多多耗费的体力最少,并输出这个最小的体力耗费值。
例如有 3 3 3 种果子,数目依次为 1 1 1, 2 2 2, 9 9 9。可以先将 1 1 1 、 2 2 2 堆合并,新堆数目为 3 3 3,耗费体力为 3 3 3。接着,将新堆与原先的第三堆合并,又得到新的堆,数目为 12 12 12,耗费体力为 12 12 12。所以多多总共耗费体力 = 3 + 12 = 15 =3+12=15 =3+12=15。可以证明 15 15 15 为最小的体力耗费值。
共两行。
第一行是一个整数 n ( 1 ≤ n ≤ 10000 ) n\ (1 \leq n \leq 10000) n (1≤n≤10000),表示果子的种类数。
第二行包含 n n n 个整数,用空格分隔,第 i i i 个整数 a i ( 1 ≤ a i ≤ 20000 ) a_i\ (1 \leq a_i \leq 20000) ai (1≤ai≤20000) 是第 i i i 种果子的数目。
一个整数,也就是最小的体力耗费值。输入数据保证这个值小于 2 31 2^{31} 231。
3
1 2 9
15
对于 30 % 30\% 30% 的数据,保证有 n ≤ 1000 n \le 1000 n≤1000;
对于 50 % 50\% 50% 的数据,保证有 n ≤ 5000 n \le 5000 n≤5000;
对于全部的数据,保证有 n ≤ 10000 n \le 10000 n≤10000。
采取贪心策略,每次可以将数量最少的两堆果子合并,直到最后只剩下一堆果子,这样即可保证耗费的体力值最小。
在这种策略下,可以很容易想到使用排序来解决这道题。但是每次都仅取排序结果的最小值和次小值相加,还要更新数组再排序,有超时的风险。因此可以采取C++的STL,将每次输入的数据用优先队列 (priority queue) 存储,优先队列中的每个元素都有优先级,而优先级高的将会先出队。定义优先队列priority_queue
,则每次优先出队的元素就是队列中的最小值和次小值。这样就可以很轻松地解决本题。
Language: C++
#include
#include
using namespace std;
int main() {
int n;
cin >> n;
priority_queue<int, vector<int>, greater<int>> q;
while (n--) {
int a;
cin >> a;
q.push(a);
}
int ans = 0;
while (q.size() > 1) {
int t1 = q.top();
q.pop();
int t2 = q.top();
q.pop();
ans += t1 + t2;
q.push(t1 + t2);
}
cout << ans << endl;
return 0;
}
观察下面的数字金字塔。
写一个程序来查找从最高点到底部任意处结束的路径,使路径经过数字的和最大。每一步可以走到左下方的点也可以到达右下方的点。
7
3 8
8 1 0
2 7 4 4
4 5 2 6 5
在上面的样例中,从 7 → 3 → 8 → 7 → 5 7 \to 3 \to 8 \to 7 \to 5 7→3→8→7→5 的路径产生了最大。
第一个行一个正整数 r r r,表示行的数目。
后面每行为这个数字金字塔特定行包含的整数。
单独的一行,包含那个可能得到的最大的和。
5
7
3 8
8 1 0
2 7 4 4
4 5 2 6 5
30
对于 100 % 100\% 100% 的数据, 1 ≤ r ≤ 1000 1\le r \le 1000 1≤r≤1000,所有输入在 [ 0 , 100 ] [0,100] [0,100] 范围内。
提到动态规划,我脑海中出现的第一道题就是这道题。它历史悠久,最早可以追溯到1994年国际信息学奥林匹克竞赛 (IOI) 的 The Triangle,而将近30年之后,曾经的 IOI 竞赛题已经变成了动态规划的入门必做题。
对三角形 t r i tri tri 的行和列均从 0 0 0 开始编号,其第 i i i 行第 j j j 列记作 ( i , j ) (i,j) (i,j)。用 d p ( i , j ) dp(i,j) dp(i,j) 表示从三角形顶部走到位置 ( i , j ) (i,j) (i,j) 的最小路径和。由于每一步只能移动到下一行相邻的位置上,因此要想走到位置 ( i , j ) (i,j) (i,j),上一步就只能在位置 ( i − 1 , j − 1 ) (i - 1, j - 1) (i−1,j−1) 或者位置 ( i − 1 , j ) (i - 1, j) (i−1,j)。在这两个位置中,选择一个路径和较大的来进行转移,有
d p ( i , j ) = max ( d p ( i − 1 , j − 1 ) , d p ( i − 1 , j ) ) + t r i i , j dp(i,j) = \max (dp(i-1,j-1), dp(i-1,j)) + tri_{i,j} dp(i,j)=max(dp(i−1,j−1),dp(i−1,j))+trii,j
特别地,当 j = 0 j=0 j=0 或 j = i j=i j=i 时,上述状态转移方程中有一些项是没有意义的,此时状态转移方程为
d p ( i , 0 ) = d p ( i − 1 , 0 ) + t r i i , 0 d p ( i , i ) = d p ( i − 1 , i − 1 ) + t r i i , i dp(i,0)=dp(i-1,0)+tri_{i,0}\\ dp(i,i)=dp(i-1,i-1)+tri_{i,i} dp(i,0)=dp(i−1,0)+trii,0dp(i,i)=dp(i−1,i−1)+trii,i
最终的答案即为 d p ( n − 1 , 0 ) dp(n-1,0) dp(n−1,0) 到 d p ( n − 1 , n − 1 ) dp(n-1,n-1) dp(n−1,n−1) 中的最大值。
本解法的时间复杂度为 O ( n 2 ) O(n^2) O(n2),空间复杂度为 O ( n 2 ) O(n^2) O(n2),其中 n n n 为三角形的行数。
Language: C++
#include
#include
#include
using namespace std;
int main() {
int n;
cin >> n;
vector<vector<int> > tri(n, vector<int>(n, 0));
for (int i = 0; i < n; i++) {
for (int j = 0; j <= i; j++) {
cin >> tri[i][j];
}
}
vector<vector<int> > dp(n, vector<int>(n, 0));
dp[0][0] = tri[0][0];
for (int i = 1; i < n; i++) {
dp[i][0] = tri[i][0] + dp[i - 1][0];
for (int j = 1; j < i; j++) {
dp[i][j] = max(dp[i - 1][j - 1], dp[i - 1][j]) + tri[i][j];
}
dp[i][i] = dp[i - 1][i - 1] + tri[i][i];
}
cout << *max_element(dp[n - 1].begin(), dp[n - 1].end()) << endl;
return 0;
}
观察上一种解法的状态转移方程,不难发现 d p ( i , j ) dp(i,j) dp(i,j) 只与 d p ( i − 1 , ⋯ ) dp(i-1,\cdots) dp(i−1,⋯) 有关,而与之前的状态无关,因此我们不必存储之前的状态,优化状态转移方程,从而进一步降低空间复杂度。
从 i i i 到 0 0 0 递减枚举 j j j,这样我们只需要一个长度为 n n n 的一维数组就可以完成状态转移。之所以递减枚举,是因为当我们在计算位置 ( i , j ) (i, j) (i,j) 时, d p ( j + 1 ) dp(j+1) dp(j+1) 到 d p ( i ) dp(i) dp(i) 已经是第 i i i 行的值,而 d p ( 0 ) dp(0) dp(0) 到 d p ( j ) dp(j) dp(j) 仍然是第 i − 1 i-1 i−1 行的值,此时有状态转移方程
d p ( j ) = max ( d p ( j − 1 ) , d p ( j ) ) + t r i i , j dp(j)=\max(dp(j−1),dp(j))+tri_{i,j} dp(j)=max(dp(j−1),dp(j))+trii,j
如果递增枚举 j j j,那么在计算位置 ( i , j ) (i, j) (i,j) 时, d p ( 0 ) dp(0) dp(0) 到 d p ( j − 1 ) dp(j-1) dp(j−1) 已经是第 i i i 行的值,使用上面的状态转移方程,则是在 ( i , j − 1 ) (i, j-1) (i,j−1) 和 ( i − 1 , j ) (i-1, j) (i−1,j) 中进行选择,这显然是错误的。
本解法的时间复杂度为 O ( n 2 ) O(n^2) O(n2),空间复杂度为 O ( n ) O(n) O(n),其中 n n n 为三角形的行数。这样只使用了 n n n 的空间存储状态,减少了空间消耗。
Language: C++
#include
#include
#include
using namespace std;
int main() {
int n;
cin >> n;
vector<vector<int>> tri(n, vector<int>(n, 0));
for (int i = 0; i < n; i++) {
for (int j = 0; j <= i; j++) {
cin >> tri[i][j];
}
}
vector<int> dp(n, 0);
dp[0] = tri[0][0];
for (int i = 1; i < n; i++) {
dp[i] = dp[i - 1] + tri[i][i];
for (int j = i - 1; j > 0; j--) {
dp[j] = max(dp[j - 1], dp[j]) + tri[i][j];
}
dp[0] += tri[i][0];
}
cout << *max_element(dp.begin(), dp.end()) << endl;
return 0;
}
这道题还可以采取自底向上的策略,从三角形的倒数第二行开始向顶层遍历,对该行的每个元素加上 max ( t r i i + 1 , j , t r i i + 1 , j + 1 ) \max(tri_{i+1,j},tri_{i+1,j+1}) max(trii+1,j,trii+1,j+1),即与其下一行相邻的位置对应元素的较大值。这样,当我们遍历到最顶层时, t r i 0 , 0 tri_{0,0} tri0,0 即为最终答案。
本解法的时间复杂度为 O ( n 2 ) O(n^2) O(n2),其中 n n n 为三角形的行数。空间复杂度为 O ( n ) O(n) O(n),所有操作均为原地修改三角形数组,没有使用额外的空间。
Language: C++
#include
#include
using namespace std;
int main() {
int n;
cin >> n;
vector<vector<int>> tri(n, vector<int>(n, 0));
for (int i = 0; i < n; i++) {
for (int j = 0; j <= i; j++) {
cin >> tri[i][j];
}
}
for (int i = n - 2; i >= 0; i--) {
for (int j = 0; j <= i; j++) {
tri[i][j] += max(tri[i + 1][j], tri[i + 1][j + 1]);
}
}
cout << tri[0][0] << endl;
return 0;
}
奶牛们开始了新的生意,它们的主人约翰想知道它们到底能做得多好。这笔生意已经做了 N ( 1 ≤ N ≤ 100 , 000 ) N\ (1\le N\le 100,000) N (1≤N≤100,000) 天,每天奶牛们都会记录下这一天的利润 P i ( − 1000 ≤ P i ≤ 1000 ) P_i\ (-1000 \le P_i \le 1000) Pi (−1000≤Pi≤1000)。
约翰想要找到奶牛们在连续的时间期间(至少一天)所获得的最大的总利润,请你写一个计算最大利润的程序来帮助他。
第一行,一个整数 N N N,表示天数。
接下来 N N N 行,每行一个整数 P i P_i Pi。
一个整数,表示最大的总利润。
7
-3
4
9
-2
-5
8
-3
14
分析题意可知,题目要求找出和最大的(连续)子数组。对于数组p
,我们可以定义状态转移方程 d p dp dp,表示以p[i]
为结尾的最大子数组和为 d p ( i ) dp(i) dp(i)。假设我们已经求出了 d p ( i − 1 ) dp(i - 1) dp(i−1) 的值,那么 d p ( i ) dp(i) dp(i) 有
既然要求最大子数组和,当然要选择更大的结果,即
d p ( i ) = max ( d p ( i − 1 ) + p i , p i ) dp(i) = \max(dp(i - 1) + p_i, p_i) dp(i)=max(dp(i−1)+pi,pi)
这样, d p dp dp 的最大值即为本题所求结果。
Language: C++
#include
#include
#include
using namespace std;
int main() {
int n;
cin >> n;
vector<int> p(n);
for (int i = 0; i < n; i++) {
cin >> p[i];
}
int ans = INT_MIN;
vector<int> dp(n);
dp[0] = p[0];
for (int i = 1; i < n; i++) {
dp[i] = max(dp[i - 1] + p[i], p[i]);
ans = max(dp[i], ans);
}
cout << ans << endl;
return 0;
}
观察上面的代码,不难发现 d p ( i ) dp(i) dp(i) 仅与 d p ( i − 1 ) dp(i - 1) dp(i−1) 的状态有关。因此我们可以进行状态压缩,将空间复杂度从 O ( n ) O(n) O(n) 降低为 O ( 1 ) O(1) O(1)。
Language: C++
#include
#include
#include
using namespace std;
int main() {
int n;
cin >> n;
vector<int> p(n);
for (int i = 0; i < n; i++) {
cin >> p[i];
}
int ans = INT_MIN;
int dp = p[0];
for (int i = 1; i < n; i++) {
dp = max(dp + p[i], p[i]);
ans = max(dp, ans);
}
cout << ans << endl;
return 0;
}