五月算法预习题解

P1024 一元三次方程求解
分析

一定存在3个不同的实数解, 且当f(x1) * f(x2) < 0 时x1 与 x2之间存在解 由于没两个解之间的差距一定大于1 那么为了覆盖所有的数据我们可以每次移动0.5
特别的 我们用穿线法分析其单调性可以发现 在我们left 到 right 区间内在有解的情况下一定是有单调性的 所有我们可以用二分去找到最终的解

代码

代码为了方便写未做优化, 可以使用仿函数优化两个二分逻辑


double cal(double l) {
	double f1 = a * pow(l, 3) + b * pow(l, 2) + c * l + d;
	return f1;
}

int main() {
	unordered_map<double, bool> used;
	double left = -101, right = -100;
	cin >> a >> b >> c >> d;
	for (; right <= 100; right += 0.5, left += 0.5) {
		if (check(right, left)) {
		// 这里判断该区间的单调性
			if (cal(right) > cal(left)) {
				double l = left, r = right;
				while (r - l > esp) {
					double mid = (l + r) / 2;
					if (cal(mid) <= 0) l = mid;
					else r = mid;
				}
				if (!used[l])
				printf("%.2lf ", l), used[l] = true;
			}
			else {
				double l = left, r = right;
				while (r - l > esp) {
					double mid = (l + r) / 2;
					if (cal(mid) <= 0) r = mid;
					else l = mid;
				}
				if (!used[l])
				printf("%.2lf ", l), used[l] = true;
			}
		}
	}
	return 0;
}

P1115 最大子段和
分析

这题以前讲过 就是一道简单的dp
f[i] = max(f[i], f[i] + f[i - 1])
时间复杂度O(n)

但是如果我们用分治的方法来分析这题的话 可以得出:
对于一个区间 其最大值分为三种情况
1 最大子段全在左半边
2 全在右半边
3 跨越中间

对于1 我们去递归右区间时会直接返回回来, 左边同理
对于中间 从中间 往左找最大 右找最大 加起来就好
返回他们三个中的最大值 即为 该区间的最大字段
时间复杂度O(nlogn)

代码

只展示dp代码

#include <iostream>

using namespace std;

const int N = 100010;
const int FINF = -0x3f3f3f3f;

int q[N];
int dp[N];
int n;

int main() {
	cin >> n;
	for (int i = 1; i <= n; ++ i) cin >> q[i];
	int res = FINF;
	dp[1] = q[1];
	for (int i = 2; i <= n; ++ i) {
		q[i] = max(q[i], q[i] + q[i - 1]);
		res = max(res, q[i]);
	}
	cout << res;
	return 0;
}

P2082 区间覆盖(加强版)

该题与一本通上的区间合并一致 只是需要的答案不同

分析

只要将全部的区间能合并在一起的合起来再算出区间的大小然后加起来就可以了

将区间以左端点排序 我们发现 对于任意两个区间只有两种情况
1 可以合并 (l <= cr)
2 不能合并 (l > cr)
对于1 我们只需要维护最打的cr 即可
对于2 我们需要将之前的区间放入答案中再去处理下一个合并的区间

代码

代码一个问题 就是数据范围最大为 10^7
所以long long 就可以存了 大家可自行忽视高精度

#include <iostream>
#include <vector>
#include <algorithm>

using namespace std;

const int N = 100010;

typedef long long LL;
typedef pair<LL, LL > PLL;
#define x first
#define y second

const LL INF = 0x3f3f3f3f3f3f3f3f;

LL res[2];

PLL q[N];

void add(LL a[], LL b) {
	a[0] += b;
	LL t = a[0] / (LL)1e18;
	a[0] %= (LL)1e18;
	a[1] += t;
}

int main() {
	LL cl = -INF, cr = -INF;
	int n;
	cin >> n;
	for (int i = 0;i < n; ++i)
		cin >> q[i].x >> q[i].y;
	sort(q, q + n);
	for (int i = 0; i < n; ++i) {
		LL l = q[i].x, r = q[i].y;
		if (l > cr + 1) {
			if (cl != -INF)
				add(res, cr - cl + 1);
			cl = l, cr = r;
		}
		cr = max(cr, r);
	}
	if (cl != -INF)
	add(res, cr - cl + 1);
	if (res[1]) printf("%lld%018lld", res[1], res[0]);
	else 
	cout << res[0];
	return 0;
}

OR25 左右最值最大差

牛客上只找到了这个题的题目与求最值想相似

先讲思路 找到最大值然后直接比较最大值和左右端点值的差, 找出最大值即可

证明 :

首先我们找到的最大值已经确定了我们需要选择的某一个区间

证明:

答案为左右两个区间的最大值的差, 那么作为数组的最大值, 无论怎么分都会成为某一个区间的最大值

接下来我们分析另外一个区间:

首先我们分析右端点 发现只有两种情况

右端点 > 右端点到最大值之间的数
右端点 < 右端点到最大值之间的数
对于第一种情况 如果我们将区间扩大 即不只包含右端点 那么会发现无论怎么改变该区间的最大值一定为右端点(因为到最大值之间右端点的值是最大的)

对于第二种情况 如果我们将端点扩张 那么一定会导致该区间的最大值变大 使得最大值 减 该区间的最大值的差减小 不符和题目求

对于左端点同理

综上 : 另一个区间一定为左右端点中的某一个端点

特别的 当最大值为左右端点中的某一个时 由于自己减去自己为0 一定小于自己减去另一个端点, 所以不需要特判

class MaxGap {
public:
    int findMaxGap(vector<int> A, int n) {
       int ma = A[0];
       for(auto e : A)
           ma = max(ma, e);
        return max(ma - A.front(), ma - A.back());
    }
};

这题还有一个比较常规的做法:

我们直接存储每个点到右端点之间的最大值 然后遍历数组的同时算右端点的最大值 最后边遍历边找出答案

两种方法都是O(n)的,也许第一个代码量确实比较小,思维也更巧妙 但是还是那句话 能做对题目的算法就是好算法

class MaxGap {
public:
    int findMaxGap(vector<int> A, int n) {
        vector<int> rma(A.size());
        for (int i = A.size() - 1, ma = -0x3f3f3f3f; i >= 0; -- i)
            ma = max(ma, A[i]), rma[i] = ma;
        int res = 0;
        for (int i = 1, ma = A[0]; i < A.size(); ++ i) {
            res = max(res, abs(ma - rma[i]));
            ma = max(ma, A[i]);
        }
        return res;
    }
};

P1969 积木大赛
分析

对于一段倒v形的高楼 我们需要的最少次数一定时最高的那一段
而对于紧接着的下一块区域 由于会受到当前区块的影响 所有最小次数为 当前区块的最高值减去上一个区块连接到自己时的区块高度
例如
[1 2 3 2 1] [2 3 4 3 2 1]
由于操作第一次的时候一定会尽量选着更加长的区间进行加 所有 第一次操作后变为:
[0, 1, 2, 1, 0, 1, 2, 3, 2, 1, 0]
可见还需要操作 2 + 3次

代码
#include <iostream>

using namespace std;

typedef long long LL;

const int N = 1000010;

LL q[N], res;
int n;

int main() {
	cin >> n;
	for (int i = 0; i < n; ++i)
		cin >> q[i];
	int last = 0;
	for (int i = 0; i < n; ++i) {
		for (; i < n; ++i)
			if (q[i + 1] <= q[i]) break;
		res += q[i] - last;
		for (; i < n; ++i)
			if (q[i + 1] > q[i]) break;
		last = q[i];
	}
	cout << res;
	return 0;
}


P8537 花如幻想一般
分析

很明显 对于区间反转 我们只有两种情况
1 反转
2 不反转
不存在反回去又反回来 因为可以想到这样没有意义只会徒增操作次数

对于两段数的需要的操作次数 显然为 abs(两者之差)

所有我们之间算出反转和不反转需要的值找出最小的一个即可

代码:
#include <iostream>
#include <cmath>

using namespace std;

const int N = 1000010;

int q[N], n;

int main() {
	cin >> n;
	n *= 2;
	for (int i = 0; i < n; ++i)
		cin >> q[i];
	int l = 0, r = n / 2;
	int res1 = 0, res2 = 0;
	for (int i = 0; i < n / 2; ++i)
		res1 += q[i] != q[n / 2 + i];
	for (int i = 0; i < n / 2; ++i)
		res2 += q[i] != q[n - i - 1];
	cout << min(res1, res2 + 1 );
	return 0;
}

P8872 D-莲子的物理热力学
分析

每次只能将最大值变最小或者将最小值变最大, 那么我们最终得到的答案一定是排序后 某个 r - l, 所以我们只需要找到符合要求的最小的r - l。
我们想想如何将最终答案变为l ~ r
1 将 l 左边的数全变大 再将 r 右边的数 全变小(这一步包含了先前 l 移过来的数)
2 反过来操作

所以我们直接发现将最小操作其实就是 l左边的数 + r右边的数 + 他们两者的最小值

所以我们直接用双指针去找最小的 r - l 即可

代码
#include <iostream>
#include <algorithm>

using namespace std;

const int N = 100010;

typedef long long LL;

LL q[N], n, k;

bool check(LL l, LL r) {
	int nums = l + n - r - 1 + min(l, n - r - 1);
	return nums <= k && r >= l;
}

int main() {
	cin >> n >> k;
	for (int i = 0; i < n; ++i)
		cin >> q[i];
	sort(q, q + n);
	LL res = 0x3f3f3f3f3f3f3f3f;
	for (int l = 0, r = 0; l < n; ++l) {
		for (; r < n && !check(l, r); ++r);
		if (r < n) res = min(res, q[r] - q[l]);
	}
	cout << res << endl;
	return 0;
}

P2249 查找
分析

二分裸题

代码
#include <iostream>

using namespace std;

const int N = 1000010;

int q[N];
int n, m;

int main() {
	cin >> n >> m;
	for (int i = 1; i <= n; ++i)
		scanf("%d", &q[i]);
	int x;
	while (m--) {
		scanf("%d", &x);
		int l = 1, r = n;
		while (l < r) {
			int mid = l + r >> 1;
			if (q[mid] >= x) r = mid;
			else l = mid + 1;
		}
		if (q[l] == x) cout << l << " ";
		else cout << -1 << " ";
	}
	return 0;
}


P2678 跳石头
分析

我记得我之前做这题的时候用的是记搜, 但是没有处理连续跳跃所以WR了,如果考虑上了时间复杂度应该是 MN 的 , 考虑到剪枝可能可以过,但是懒得写了所以放弃了);

下面说正解, 二分答案。
什么时候会去考虑二分? 当数据具有二段性的时候 我们可以用二分去找到其边界
本体的哪里具有二段性? 没错 答案
我们设 区间的左边都小于答案 右边都大于等于答案
当二分到右边时 若 跳跃的最远距离 <= 二分结果 那么我们就认为答案不可能在其右边(但可能时当前值) 因为当前结果可行且小于右边所以(更佳)
当二分到左边时 由于答案小于当前二分结果 所以无论如何都无法满足条件 所以答案一定大于当前二分结果(不包含当前值)因为当前值都已经不符合要求了, 左边更小的更不行
最终就可以二分出答案

代码
#include <iostream>

using namespace std;

const int N = 50010;

int L, m, n;
int q[N];

bool check(int mid) {
	int cur = 0, sto = 0;
	for(int i = 0; i <= n; ++ i) {
		if (q[i] - cur < mid) sto ++;
		else cur = q[i];
	}
	return sto <= m;
}

int main() {
	cin >> L >> n >> m;
	for (int i = 0; i < n; ++ i)
		cin >> q[i];
	q[n] = L;
	int l = 0, r = L;
	while (l < r) {
		int mid = l + r + 1 >> 1;
		if (check(mid)) l = mid;
		else r = mid - 1;
	}
	cout << l;
	return 0;
}

P8749 杨辉三角形
分析

这里就不得不提杨辉三角形的一个性质了——除了第一个数其他的每个数都等于c(n, m)
其中 n代表行数 m代表列数

知道了这个就很容以想到去枚举n或m 然后去二分m或n。
我们来分析一下这两种方法的区别
1 枚举 m 去二分 n 期望的时间复杂度是 nlogn的 其中n为数据大小 因为我们只知道 c(n, 1)能一定是n 所以为了找到所有的数 就不得不枚举 10**9次 但是我试了一下本题的数据较水 我们控制枚举5000次以上就可以过 所有这个方法也可以

2 枚举n 去二分 m 这个就和上面没什么很大的区别 区别的就是 我们将大的值拿去二分了 所以枚举n会更加的快 因为 我们可以通过程序算出
在这里插入图片描述
也就是我们只需要枚举16次即可 而1000000000会拿去二分

综上 :
两者的差距只是二分谁的问题 对于第一种方法 很多的枚举和二分都是显然大于答案而无用的。

代码

第一个为不剪枝 直接控制上限的第一种方法
第二个为第二种方法

1
#include <iostream>

using namespace std;

typedef unsigned long long LL;

LL n;

LL c(double n, double m) {
	double res = 1;
	for (int i = 0; i < m; ++i)
		res *= (n - i) / (m - i);
	return res;
}

int main() {
	cin >> n;
	if (n == 1) {
		cout << 1 << endl;
		return 0;
	}
	for (int i = 1; i < 16; ++i) {
		int l = 0, r = i * 2 + 1;
		while (l < r) {
			int mid = l + r + 1 >> 1;
			if (c(mid, i) <= n) l = mid;
			else r = mid - 1;
		}
		if (c(l, i) == n) {
			cout << (1 + l) * l / 2 + i + 1 << endl;
			break;
		}
	}
	return 0;
}
2
#include <iostream>

using namespace std;

typedef long long LL;

const LL INF = 0x3f3f3f3f3f3f3f3f;

LL n;

LL c(double a, double b) {
	double res = 1;
	for (int i = 0; i < b; ++i) {
		res *= (a - i) / (b - i);
		if (res > n) return INF;
	}
		
	return res;
}

int main() {
	cin >> n;
	if (n == 1) {
		cout << 1 << endl;
		return 0;
	}
	LL res = INF;
	for (int i = 1; i < 16; ++i) {
		LL l = 0, r = 1000000010;
		while (l < r) {
			LL mid = l + r + 1 >> 1;
			if (c(mid, i) <= n) l = mid;
			else r = mid - 1;
		}
		if (c(l, i) == n) {
			res = min(res, (1 + l) * l / 2 + i + 1);
		}
	}
	cout << res;
	return 0;
}
P8873 E-梅莉的市场经济学
分析

我们可以找到这个规律 每次凸点 和 凹点都会增加1 这样就会导致 该区间的长度增加4
所以这样我就可以直接去二分给出的数据位于哪一个区间, 然后对于每个区间又4个点
我们再直接再根据其所在的区间数和起始位置划分着4个区间 然后再根据偏移量算出最终结果

代码
#include <iostream>

using namespace std;

typedef long long LL;
typedef unsigned long long ULL;

const LL N = 2000000000;

ULL a;

bool check(ULL x) {
	x = 1 + 4 * (x - 1);
	LL sum = (1 + x) * (((x - 1) / 4) + 1) / 2;
	return sum <= a;
}

int main() {
	int q;
	cin >> q;
	while (q--) {
		cin >> a;
		LL l = 0, r = N;
		while (l < r) {
			ULL mid = ((r - l) / 2 + 1) + l;
			if (check(mid)) l = mid;
			else r = mid - 1;
		}
		ULL sum = 1 + 4 * (l - 1);
		ULL st = (1 + sum) * ((sum - 1) / 4 + 1) / 2;
		st += 1;
		ULL q[5] = { st, st + l, st + 2 * l, st + 3 * l, st + 4 * l };
		LL p[5] = { 0, l, 0, -l, 0 };
		LL res = 0;
		for (int i = 0; i < 4; ++i) {
			if (a >= q[i] && a <= q[i + 1]) {
				res = p[i];
				ULL dx = a - q[i];
				if (i == 0 || i == 3) res += dx;
				if (i == 1 || i == 2) res -= dx;
				break;
			}
		}
		cout << res << endl;
	}
	return 0;
}

P2800 又上锁妖塔
分析

根着题目来就好了

当前位置只能由前两个区间跳过来 或者由上一个区间爬上来

由于我们不能连续跳 但休息一下就能继续跳 所以这里涉及一个简单的状态机

我们用0/1 表示当前能/否跳跃
1 可以由 前两个区间的0 推出来
0 可以由上一个区间的0 / 1推出来

代码
#include <iostream>

using namespace std;

const int N = 1000010;

int  q[N];
int n;
int dp[N][2];

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

P1095 守望者的逃离
分析

这题也可以用状态机dp
状态则是我们当前的体力值 时间复杂度 n * m 理论能过, 但是还是MLE两个点,也就是开二维数组内存超了
所以 我们只能去想能否压一维,由于当前时间一定由上一个时间推出来所以我们是可以压一维的 但是必须的用滚动数组, 因为直接压一维的话会导致数据被覆盖, 可惜的是测了很多次最后一个点一直会RE 但是数组并不可能发生越界 唉 所以只能另寻方法了

代码1
#include <iostream>

using namespace std;

const int N = 300010, M = 10100;
const int INF = 0x3f3f3f3f;

int dp1[M];
int dp2[M];
int m, s, t;

int main() {
	cin >> m >> s >> t;
	bool flag = false;
	for (int i = 0; i <= t; ++i)
		for (int j = 0; j <= m; ++j)
			dp1[j] = -INF;
	dp1[m] = dp2[m] =  0;
	for (int i = 1; i <= t; ++i) {
		for (int j = 0; j <= m; ++j) {
			if (i % 2 == 1) {
				dp1[j] = dp2[j] + 17;
				if (j + 10 <= m) dp1[j] = max(dp1[j], dp2[j + 10] + 60);
				if (j >= 4) dp1[j] = max(dp1[j], dp2[j - 4]);
				if (dp1[j] >= s) {
					m = i;
					flag = true;
				}
			}
			else {
				dp2[j] = dp1[j] + 17;
				if (j + 10 <= m) dp2[j] = max(dp2[j], dp1[j + 10] + 60);
				if (j >= 4) dp2[j] = max(dp2[j], dp1[j - 4]);
				if (dp2[j] >= s) {
					m = i;
					flag = true;
				}
			}
		}
		if (flag) break;
	}
	int res = 0;
	for (int i = 0; i <= m; ++i) res = max(res, max(dp1[i], dp2[i]));
	if (flag) cout << "Yes" << endl << m;
	else cout << "No" << endl << res << endl;
	return 0;
}

我们发现,在状态机dp时,很多的魔力值其实时不可以被推出来了的 所以我们想能否将魔力值去掉?显然由于不存在任何权值的限制 所以无论任何的时候用闪现都是一样的效果,所以我们应当是能用闪现就用闪现, 所以我们直接记录全程闪现能走的距离, 然后可以推出到当前时间只能是上一秒的最优值 + 走到当前值, 或 之间闪现到当前时间即:
q[i] = max(q[i], q[i - 1] + 17)

代码2
#include <iostream>

using namespace std;

const int N =  3 * 100010;

int q[N];
int m, s, t;

int main() {
	cin >> m >> s >> t;
	for (int i = 1; i <= t; ++ i) {
		if (m >= 10) q[i] = q[i - 1] + 60, m -= 10;
		else q[i] = q[i - 1], m += 4;
	}
	for (int i = 1; i <= t; ++ i) {
		q[i] = max(q[i], q[i - 1] + 17);
		if (q[i] >= s) {
			cout << "Yes" << endl << i;
			return 0;
		}
	}
	cout << "No" << endl << q[t];
	return 0;
}

P7074 方格取数
分析

由于我们同时可以走三个方向, 所以直接的dp会导致无法进行(因为去到的子状态可能未被推出来)
考虑预处理单列的上往下最大值, 和下往往最大值
对于上往下最大值 我们可以发现他一定不可能由下往上推出来, 因为这样必定会走重复路
所以只会由上和左推出来, 但是左边能被推出来吗? 我们发现第一列的最优解只会由上下推出, 而上不可能, 所以只能向下, 显然第一个列的最优解可以被推出来,那么对于第二列就可以利用上下左推出最优解了, 同理往后直到推出答案, 对于下往上同理
所以我们发现所有的状态都可以被推出来了

代码
#include <iostream>
#include <cstring>

using namespace std;

const int N = 1010;

typedef long long LL;

LL q[N][N], n, m;
LL up[N][N], down[N][N];
LL f[N][N];

int main() {
	cin >> n >> m;
	for (int j = 0; j < N; ++j) {
		up[0][j] = down[0][j] = -0x3f3f3f3f;
		up[n + 1][j] = down[n + 1][j] = -0x3f3f3f3f;
	}
	for (int i = 1; i <= n; ++i)
		for (int j = 1; j <= m; ++j)
			cin >> q[i][j];
	for (int i = 0; i < N; ++i)
		for (int j = 0; j < N; ++j)
			f[i][j] = -0x3f3f3f3f;
	down[0][1] = 0;
	for (int j = 1; j <= m; ++j) {
		for (int i = n; i >= 1; --i)
			up[i][j] = max(up[i + 1][j], f[i][j - 1]) + q[i][j];
		for (int i = 1; i <= n; ++i)
			down[i][j] = max(down[i - 1][j], f[i][j - 1]) + q[i][j];
		for (int i = 1; i <= n; ++i)
			f[i][j] = max(up[i][j], down[i][j]);
	}
	cout << f[n][m];
	return 0;
}

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值