第二次上机实验解题报告

49 篇文章 1 订阅
30 篇文章 1 订阅

感觉这次题目也是挺有趣的,虽然我忘了学三元组,幸好乱搞也能通过第二题。相比于上次上机实验,这次上机实验中的问题时间空间都卡得很严,所以最优解法相对也比较唯一。

T1 数列查询

题意简述

定义了一个数列:

f n = { 10 , n = 1 ⌊ f n − 1 ∗ 11 10 ⌋ , n > 1 f_n=\left\{\begin{aligned}10,&n=1\\ \left\lfloor f_{n-1}*\frac{11}{10}\right\rfloor,&n>1 \end{aligned}\right. fn=10,fn11011,n=1n>1

给出一个长度为 n n n 的正整数数列 a 1 , a 2 , ⋯   , a n a_1, a_2, \cdots, a_n a1,a2,,an,求 f a 1 , f a 2 , ⋯   , f a n f_{a_1}, f_{a_2}, \cdots, f_{a_n} fa1,fa2,,fan。输入保证,输出的答案一定在 32 位 int 范围内。时间限制 10 m s 10ms 10ms,空间限制 1 M B 1MB 1MB

解题思路

感觉到 a i a_i ai 不可能很大,这一直觉来自于渐进复杂度分析中的这一结论:

∀ k > 1 , ∀ j , n j = O ( k n ) \forall k > 1, \forall j , n^j=O(k^n) k>1,j,nj=O(kn)

简而言之就是,指数渐进增长 无论指数多小,增长都很快,都比任何多项式快。

考虑数量级近似:

f n ≈ 10 × ( 11 10 ) n − 1 f_n\approx 10\times\left(\frac{11}{10}\right)^{n-1} fn10×(1011)n1

由于输入保证 f a i < 2 31 f_{a_i}<2^{31} fai<231,因此不妨令

f a i ≈ 10 × ( 11 10 ) a i − 1 < 2 31 f_{a_i} \approx 10\times\left(\frac{11}{10}\right)^{a_i-1}<2^{31} fai10×(1011)ai1<231

解得 a i < 202.29 a_i < 202.29 ai<202.29,但是由于数量级近似有误差,我们应该对得到的界适当放宽,不妨令 a i ≤ 1000 a_i\leq 1000 ai1000(其实这个有点放的太大了,不过,反正有足足十毫秒可以浪费是吧。)此时,可以先用递推的方法,对所有小于等于以前的正整数计算出对应的 f f f 值,再一起处理输出。

p.s. 虽然在计算的过程中,当 i i i 足够大时 f i f_i fi 已经发生了自然溢出,但是由于题目保证输出的答案一定在 32 位 int 范围内,因此那些错误的 f f f 值并不会影响程序本身的正确性。

代码实现

#include <cstdio>

int F[1000 + 3];

int main() {
	int q; scanf("%d", &q);
	F[1] = 10;
	for(int i = 2; i <= 1000; i ++) { /// 预处理 F[1] 到 F[1000]
		F[i] = F[i-1] * 11 / 10;
	}
	while(q --) {
		int i; scanf("%d", &i);
		printf("%d\n", F[i]);
	}
	return 0;
}

进一步验证

如果要是觉得这种近似数量级分析不可靠,也可以通过程序来找到 a i a_i ai 的取值范围。

#include <cstdio>

int F[1000 + 3];

int main() {
	F[1] = 10;
	for(int i = 2; i <= 1000; i ++) {
		F[i] = F[i-1] * 11 / 10;
		if(F[i] <= F[i-1] || F[i] <= 0) {
			printf("F[%d] = %d, F[%d] = %d\n", i, F[i], i-1, F[i-1]);
			break;
		}
	}
	return 0;
}

这个程序的输出为:

F[184] = -196842947, F[183] = 211503438

显然, F F F数组恰好在 F 184 F_{184} F184 处发生了自然溢出,因此我们可以断言 a i ≤ 183 a_i \leq 183 ai183

T2 稀疏矩阵之差

题意简述

按照行列递增的三元组的形式给出两个稀疏矩阵,用三元组的形式输出它们的差。特殊地,如果两个矩阵不能进行减法,输出 “ I l l e g a l ! Illegal! Illegal!”(不含双引号)。

矩阵的尺寸 10 ≤ n 1 , m 1 , n 2 , m 2 ≤ 5 × 1 0 4 , t 1 ≤ min ⁡ ( n 1 , m 1 ) , t 2 ≤ min ⁡ ( n 2 , m 2 ) 10\leq n_1, m_1, n_2, m_2\leq 5\times10^4, t_1\leq \min(n_1, m_1), t_2\leq \min(n_2, m_2) 10n1,m1,n2,m25×104t1min(n1,m1),t2min(n2,m2)。时间限制 100 m s 100ms 100ms,空间限制 10 M B 10MB 10MB

解题思路

双指针法合并两个稀疏矩阵即可(合并过程类似于归并排序)。但是有一些细节:比如当两个位置做差后恰好等于零的时候,最终输出的时候就不要输出这个位置了。

我们定义位置之间的比较大小的算法:

D e f : ( r 1 , c 1 ) < ( r 2 , c 2 )   i f f   ( r 1 < r 2 ) o r ( ( r 1 = r 2 ) a n d ( c 1 < c 2 ) ) Def:(r_1, c_1)<(r_2,c_2) \space iff \space (r_1<r_2)or((r_1=r_2)and(c_1<c_2)) Def:(r1,c1)<(r2,c2) iff (r1<r2)or((r1=r2)and(c1<c2))

不难证明,这是一种建立在坐标集合上的逆序关系,这是我们双指针合并的前提。

代码实现

#include <cstdio>
#include <algorithm>
using namespace std;

const int maxn = (50000 + 5)*2;

int R1[maxn], C1[maxn], V1[maxn], T1; /// 储存第一个矩阵的三元组信息 row1[], column1[], counT1[]
int R2[maxn], C2[maxn], V2[maxn], T2; /// 储存第二个矩阵的三元组信息 row2[], column2[], counT2[]
int R3[maxn], C3[maxn], V3[maxn], T3; /// 储存结果矩阵的三元组信息   row3[], column3[], counT3[]
/// 起初 T3 等于零,随着算法的进行 T3 的值会逐渐增大

int LSS(int r1, int c1, int r2, int c2) { /// less than 位置之间的 < 关系
	if(r1 == r2) return c1 < c2;
	return r1 < r2;
}
int EQU(int r1, int c1, int r2, int c2) { /// 位置之间的 = 关系
	return r1 == r2 && c1 == c2;
}

void output(int r, int c, int v) { /// 向结果矩阵中添加一个值
	T3 ++;
	R3[T3] = r;
	C3[T3] = c;
	V3[T3] = v;
}

int main() {
	int N1, M1; scanf("%d%d%d", &N1, &M1, &T1);
	for(int i = 1; i <= T1; i ++) {
		scanf("%d%d%d", &R1[i], &C1[i], &V1[i]); /// 输入第一个矩阵
	}
	int N2, M2; scanf("%d%d%d", &N2, &M2, &T2);
	for(int i = 1; i <= T2; i ++) {
		scanf("%d%d%d", &R2[i], &C2[i], &V2[i]); /// 输入第二个矩阵
	}
	if(N1 == N2 && M1 == M2) { /// 两个矩阵在两个维度的尺寸分别相同是能进行减法的充要条件
		int L = 1, R = 1;
		while(R <= T2) { /// 从小到大枚举第二个矩阵中的所有元素
			while(L <= T1 && LSS(R1[L], C1[L], R2[R], C2[R])) {
				/// 对于第一个矩阵中的所有位置 L,如果(R1[L], C1[L]) < (R2[R], C2[R])
				/// 那么对于任意 R' > R (输入保证 (R2[R], C2[R]) < (R2[R'], C2[R']))
				/// 一定都有 (R1[L], C1[L]) < (R2[R], C2[R]) < (R2[R'], C2[R']) (逆序关系的传递性)
				/// 即 (R1[L], C1[L]) < (R2[R'], C2[R'])
				/// 如果第一个矩阵中的 L 位置不能和当前的 R 位置合并,那么它就不能与其后的任意一个 R' 位置合并
				/// 因此此时应该直接输出 R1[L], C1[L] 中的值
				output(R1[L], C1[L], V1[L]);
				L ++;
			}
			if(L <= T1 && EQU(R1[L], C1[L], R2[R], C2[R])) {
				/// 此时说明 (R1[L], C1[L]) 与 (R2[L], C2[R]) 恰好能够合并
				if(V1[L] - V2[R] != 0) /// 如果这两个位置的差为 0 ,最终答案中时不应该输出的,这是一个坑
					output(R1[L], C1[L], V1[L] - V2[R]);
				L ++;
			}else {
				output(R2[R], C2[R], - V2[R]);
				/// 执行到这种分支情况,说明任何一个第一个矩阵中的非零位置都不和 (R2[R], C2[R]) 合并
				/// 由于所有的 小于 (R2[R], C2[R]) 的 (R1[L], C1[L]) 在前面的循环中已经被输出了
				/// 因此此时输出 (R2[R], C2[R]) 这个位置,能保证输出的结果矩阵三元组数列仍是行列递增的
			}
			R ++;
		}
		while(L <= T1) {
			/// 由于 第一个矩阵中可能有一些位置,它们的坐标比任何第二个矩阵中的位置都要大
			/// 这个循环可以将这部分位置直接输出
			output(R1[L], C1[L], V1[L]);
			L ++;
		}
		printf("%d %d %d\n", N1, M1, T3); /// 输出 结果矩阵的尺寸信息
		for(int i = 1; i <= T3; i ++) {
			printf("%d %d %d\n", R3[i], C3[i], V3[i]);
			/// 输出结果矩阵的三元组表示
			/// 前文的证明保证了这个三元组也是行列递增的
		}
	}else { /// 不能进行减法,输出 Illegal!
		printf("Illegal!");
	}
	return 0;
}

效率分析

由于每次比较都恰会导致一个单位的时间代价,而每次比较至少会“消耗” A 矩阵或者 B 矩阵中的一个位置,因此时间复杂度为 O ( t 1 + t 2 ) O(t_1+t_2) O(t1+t2)

T3 文字编辑

题意简述

一篇文章由 n n n 个汉字构成,汉字从前到后依次编号为 1 , 2 , ⋯ , n 1,2,\cdots,n 12n。 支持以下四种操作:

  • A   i   j A \space i \space j A i j 表示把编号为 i i i 的汉字移动编号为 j j j 的汉字之前;
  • B   i   j B \space i \space j B i j 表示把编号为 i i i 的汉字移动编号为 j j j 的汉字之后;
  • Q   0   i Q \space 0 \space i Q 0 i 为询问编号为 i i i 的汉字之前的汉字的编号;
  • Q   1   i Q \space 1 \space i Q 1 i 为询问编号为 i i i 的汉字之后的汉字的编号。

规定:(初始时) 1 1 1 号汉字前面的汉字是 n n n 号汉字, n n n 号汉字后面的汉字是 1 1 1 号汉字。

多组测试数据:测试数据组数 T ≤ 9999 T\leq 9999 T9999,每组数据的汉字总数 n ≤ 9999 n \leq 9999 n9999,每组数据的操作/询问总数 m ≤ 9999 m\leq 9999 m9999

解题思路

这不是上课讲的例题吗?跳舞链

p.s. 这里的跳舞链指的是双向循环链表。

代码实现

#include <cstdio>

const int maxn = 10000 + 5;
int lst[maxn], nxt[maxn];

/// lst[i] 表示编号为 i 的汉字的前一个汉字的编号
/// nxt[i] 表示编号为 i 的汉字的后一个汉字的编号

int main() {
	int T; scanf("%d", &T);
	while(T --) {
		int n, q; scanf("%d%d", &n, &q); /// 输入汉字的总数和操作/询问的总数
		for(int i = 1; i <= n; i ++) { /// 初始化双向循环链表
			lst[i] = i - 1;
			nxt[i] = i + 1;
		}
		lst[1] = n;
		nxt[n] = 1;
		while(q --) {
			char ope[2]; scanf("%s", ope);
			/// 虽然是读入一个字符,但是还是当作一个字符串读入更为靠谱
			/// 因为使用 %s 读入字符串的时候会自动忽略回车、空格等空白字符
			int i, j; scanf("%d%d", &i, &j); /// 输入一个操作
			if(ope[0] == 'A') { /// 将 i 插入到 j 前
				nxt[lst[i]] = nxt[i];
				lst[nxt[i]] = lst[i]; /// 通过一次跳舞将节点 i 脱离原链
				nxt[i] = j;
				lst[i] = lst[j]; /// 将节点 i 的前驱后继修改为新的前驱后继
				nxt[lst[i]] = i;
				lst[nxt[i]] = i; /// 通过一次跳舞 将节点 i 插入到新的前驱后继身边
			}else if(ope[0] == 'B') { /// 将 i 插入到 j 后
				nxt[lst[i]] = nxt[i];
				lst[nxt[i]] = lst[i]; /// 通过一次跳舞将节点 i 脱离原链
				lst[i] = j;
				nxt[i] = nxt[j]; /// 将节点 i 的前驱后继修改为新的前驱后继
				nxt[lst[i]] = i;
				lst[nxt[i]] = i; /// 通过一次跳舞 将节点 i 插入到新的前驱后继身边
			}else { /// 查询操作
				if(i == 0) { /// 查询节点 j 的前驱
					printf("%d\n", lst[j]);
				}else { /// 查询节点 j 的后继
					printf("%d\n", nxt[j]);
				}
			}
		}
	}
	return 0;
}

效率分析

不难分析出这种算法的时间复杂度为 O ( T ( n + m ) ) O(T(n+m)) O(T(n+m))

p.s. 使用 scanf %c 或者 getchar 或者 fgetc 等方法读入一个字符 过于精确,会导致读入一个 \r(回车) 或者 \n(换行符)或者 空格。解决这一问题的方法有很多,您把它当成一个字符串用 scanf %s 读进来不就得了。 T_T

T4 幸福指数

题意简述

给定一个长度为 n n n 的整数数列 a 1 , a 2 , ⋯ a n a_1, a_2, \cdots a_n a1,a2,an,定义 区间 [ L , R ] [L, R] [L,R]幸福指数为:

( ∑ k = L R a k ) × min ⁡ L ≤ k ≤ R ( a k ) \left(\sum_{k=L}^Ra_k\right)\times\min_{L\leq k\leq R}(a_k) (k=LRak)×LkRmin(ak)

试找到数列 { a n } \{a_n\} {an} 的所有区间中,幸福指数最大的那一个。如果有幸福指数相同的区间,优先输出长度较长的区间。如果有幸福指数相同且区间长度也相同的区间,优先输出左端点更靠左的区间。

数据范围: n ≤ 1 0 5 , 0 ≤ a i ≤ 1 0 6 n\leq 10^5, 0\leq a_i\leq 10^6 n105,0ai106。时间限制 100 m s 100ms 100ms,空间限制 64 M B 64MB 64MB

解题思路

对于每一个位置 i i i,我们令 [ L i , R i ] [L_i, R_i] [Li,Ri] 是包含 位置 i i i 且以 a i a_i ai 为最小值的所有区间中,最长的那一个。由于 a i ≥ 0 a_i \geq 0 ai0,因此不难证明,幸福指数最大的区间一定在 [ L 1 , R 1 ] , [ L 2 , R 2 ] , ⋯   , [ L n , R n ] [L_1, R_1] , [L_2, R_2], \cdots, [L_n, R_n] [L1,R1],[L2,R2],,[Ln,Rn] 之中。

稍微解释一下这个性质,在所有包含 位置 i i i 且以 a i a_i ai 为最小值的区间中, [ L i , R i ] [L_i, R_i] [Li,Ri] 是最长的一个。由于 a i > 0 a_i > 0 ai>0 所以,任取一个包含位置 i i i 且以 a i a_i ai 为最小值的区间 [ L i ′ , R i ′ ] [L_i', R_i'] [Li,Ri],我们都能说明,区间 [ L i , R i ] [L_i, R_i] [Li,Ri] 与 区间 [ L i ′ , R i ′ ] [L_i', R_i'] [Li,Ri] 具体有相同的 min ⁡ L ≤ k ≤ R ( a k ) \min_{L\leq k\leq R}(a_k) minLkR(ak),但 由于 ∑ k = L i R i a k ≥ ∑ k = L i ′ R i ′ a k \sum_{k=L_i}^{R_i} a_k \geq \sum_{k=L_i'}^{R_i'}a_k k=LiRiakk=LiRiak 而且 L i ≤ L i ′ L_i \leq L_i' LiLi, 因此 [ L i ′ , R i ′ ] [L_i', R_i'] [Li,Ri] 一定没有区间 [ L i , R i ] [L_i, R_i] [Li,Ri] 更优。

现在,问题转化为了,对于所有的 i i i,计算 L i L_i Li R i R_i Ri。从 i i i 位置开始,向左找到第一个比 a i a_i ai 小的位置 l l l,那么我们令 L i = l + 1 L_i=l+1 Li=l+1;特殊地,如果位置 i i i 左侧的所有元素都大于等于 a i a_i ai,不妨零 L i = 1 L_i = 1 Li=1。同理,从 i i i 位置开始,向右找到第一个比 a i a_i ai 小的位置 r r r,那么我们有 R i = r − 1 R_i=r-1 Ri=r1,特殊地,如果位置 i i i 右侧的所有位置都大于等于 a i a_i ai 我们令 R i = n R_i=n Ri=n

对于每一个位置,我们都要找到这个位置左侧第一个比它小的位置,这不正是单调栈解决的问题吗?

为了实现方便,在下文的程序中,我们记 L i L_i Li 为 位置 i i i 左侧第一个 比 a i a_i ai 小的元素的下标;记 R i R_i Ri 为位置 i i i 右侧第一个比 a i a_i ai 小的元素的下标(而不是前文中所说的最长的区间端点)。此时,我们的幸福指数最大的区间一定在下列区间中产生:

[ L 1 + 1 , R 1 − 1 ] , [ L 2 + 1 , R 2 − 1 ] , ⋯   , [ L n + 1 , R n − 1 ] [L_1+1, R_1-1], [L_2+1, R_2-1], \cdots, [L_n+1, R_n-1] [L1+1,R11],[L2+1,R21],,[Ln+1Rn1]

为了实现方便,我们令 a 0 = − ∞ , a n + 1 = − ∞ a_0=-\infty, a_{n+1}=-\infty a0=,an+1= 作为哨兵位置,防止上文中特殊情况情况的出现。

代码实现

#include <cstdio>
#include <stack>
using namespace std;

const int maxn = 1000000 + 7;
long long A[maxn], Pre[maxn];
int L[maxn], R[maxn];

const long long inf = 0x7f7f7f7f7f7f7f7fLL;
/// 没必要这么夸张,其实设置成 -1 就行
stack<int> s;
/// 储存一个单调栈,栈中存放的是数组中元素的下标,方便定位

int main() {
	int n; scanf("%d", &n); /// 输入数列长度
	A[0] = -inf; /// 设置哨兵位置
	A[n + 1] = -inf;
	for(int i = 1; i <= n; i ++) {
		scanf("%lld", &A[i]);
		Pre[i] = Pre[i-1] + A[i]; /// 计算前缀和数组,用来在后文中快速计算区间和
	}
	s.push(0);
	for(int i = 1; i <= n; i ++) { /// 从左向右计算 L 值
		while(A[s.top()] >= A[i]) {
			/// 当前位置左侧,那些数值大于等于当前位置的位置
			/// 不可能成为 当前位置 i 右侧的 任何位置的 L[] 值
			/// 因此我们可以把它从栈中弹出
			s.pop();
		}
		L[i] = s.top(); /// 此时 A[s.top()] 恰好小于 A[i]
		s.push(i);
	}
	while(!s.empty()) s.pop(); /// to empty
	s.push(n+1);
	for(int i = n; i >= 1; i --) { /// 从右向左计算 R 值
		while(A[s.top()] >= A[i]) {
			/// 当前位置右侧,那些数值大于等于当前位置的位置
			/// 不可能成为 当前位置 i 左侧的 任何位置的 R[] 值
			/// 因此我们可以把它从栈中弹出
			s.pop();
		}
		R[i] = s.top(); /// 此时 A[s.top()] 恰好小于 A[i]
		s.push(i);
	}
	long long ans = A[1];
	int nL = 1, nR = 1; /// [nL, nR] 用于记录答案区间,ans 是这个区间的幸福指数
	for(int i = 1; i <= n; i ++) {
		long long tmp = (Pre[R[i] - 1] - Pre[L[i]])*A[i]; /// 计算区间[L[i]+1, R[i]-1]的幸福指数
		if(tmp > ans) { /// 如果幸福指数比当前最优解大,直接更新当前最优解
			ans = tmp;
			nL = L[i] + 1;
			nR = R[i] - 1;
		}else if(tmp == ans) { /// 如果幸福指数相同,考虑区间长度和区间左端点的位置
			int dL = L[i] + 1;
			int dR = R[i] - 1;
			if(dR - dL > nR - nL) { /// 新的区间比老的区间更长,更新答案
				nL = dL;
				nR = dR;
			}else if(dR - dL == nR - nL) { /// 新的区间和老的区间一样长
				if(dL < nL) { /// 如果新的区间的左端点更靠左,则更新答案,否则不更新答案
					nL = dL;
					nR = dR;
				}
			}
		}
	}
	printf("%lld\n%d %d", ans, nL, nR);
	return 0;
}

效率分析

使用摊还分析,我们能够证明,这个算法的时间复杂度为 O ( n ) O(n) O(n)。采用核算法,不难发现,我们使用了两次单调栈扫描,每次单调栈扫描过程中,每个位置,最多进栈一次、出栈一次,造成 O ( 1 ) O(1) O(1) 的时间代价。

后记

考试结束,解题报告刚好写完,开心,完结撒花。 ^_^

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值