递归和递推
递归和递推解决问题的思考方向相反:递归关心怎样把问题分解成规模更小的相似子问题,递推关心怎么从一个规模更小的状态得到一个规模更大的状态。
洛谷P1990 覆盖墙壁
在我们已经铺好了 2 * N 块砖后,我们可以:
- 通过铺一块 I 型砖头到达
块砖头的状态
- 通过铺两块 I 型砖头到达
块砖头的状态
- 通过铺两块 L 型砖头以及若干块 I 型砖头到达
块砖头的状态,每一种结果都对应着两种方案(上下颠倒一下)
因此,我们有递推式:,我们可以继而求出另一个通项公式:
,或者我们也可以用前缀和来简化计算。
洛谷P1044 栈
首先我们想用搜索的方法来暴力求解。容易看出虽然我们要求的是“出栈序列的总数”,但是我们并不用关心具体的数字顺序是什么。这是因为在任意一个时刻,我们只能有入栈、出栈两种选择,并且这两种选择对应的结果必然不相同。
证明:考虑任意一个状态,设栈顶的元素为 top,出栈序列中已经有了 k 个元素。如果我们此时选择出栈,那么出栈序列的第 k + 1 个元素为 top。如果我们选择入栈,那么入栈的那个元素在出栈序列中必然排在 top 前面,也就是说 top 不可能成为出栈序列的第 k + 1 个元素。
根据以上的结论,我们发现真正有意义的信息其实就是“还没入栈的元素个数”和“栈中的元素个数”,因此不难写出以下的搜索代码:(初始执行search(n, 0))
long long search(int num1, int num2) {
if (num1 == 0) return 1;
else if (num2 == 0) return search(num1 - 1, 1);
else return search(num1, num2 - 1) + search(num1 - 1, num2 + 1);
}
但是这样会超时,因此我们使用记忆化搜索的策略,使用 来表示“有 i 个元素还没入栈”并且“有 j 个元素在栈中”的方案数,初始为
。
long long search(int num1, int num2) {
if (f[num1][num2] != 0);
else if (num2 == 0) f[num1][num2] = search(num1 - 1, 1);
else f[num1][num2] = search(num1, num2 - 1) + search(num1 - 1, num2 + 1);
return f[num1][num2];
}
//for (int i = 1; i <= n; ++i) f[0][i] = 1;
我们使用递归 + 优化的方式 AC 了这一题。
我们也可以使用递推的方式,我们有递推式:
因此:
for (int i = 0; i <= n; ++i) f[0][i] = 1;
for (int i = 1; i <= n; ++i) {
for (int j = 0; j <= n; ++j) {
if (j == 0) f[i][j] = f[i - 1][1];
else f[i][j] = f[i][j - 1] + f[i - 1][j + 1];
}
}
最后,让我们从数学上分析这个问题:这其实就是卡特兰数。
设 为 n 个数字出栈序列的方案数,我们第一步进行的操作无疑是将 1 入栈,此时待入栈的序列为 2 3 ... n - 1。
我们考虑数字 1 在出栈序列中的位置:若 1 排在第 k 位,那么在 1 出栈时,前 k - 1 个元素都已经历了由出栈到出栈的过程,方案数为 ,在此之后还有 n - k - 1 个元素需要经历由出栈到出栈的过程,方案数为
。由乘法原理,我们有公式:
其实就是卡特兰数,我们也有其他的表达式:
实质上,卡特兰数的模型为:执行相等数量的任意两种操作,要求在执行第 k 次操作 2 时必须先执行 k 次操作 1。这个模型有很多实际例子,比如 n 对括号的合法序列个数,n 个元素的进出栈方案数,n + 1 个叶子的无标号二叉树个数,n + 2 边形的三角剖分方案数,从原点向右或向上不穿过 y = x 走到 (n, n) 的方案数等等。
对于最后一个例子,我们可以用折线法和减法原理来解答:
我们将 y = x 看作新的 x 轴,那么我们是要从 (0, 0) 走到 (n, 0),每一步能够往右上方或者右下方走一个对角线的方向。假如说没有不穿过坐标轴的限制,那么我们的方案数其实就是要在 2n 步中选择 n 步向右上方走,为 。现在我们思考不合法的方案数,对于任意跨越坐标轴的方案,它必然与 y = -1 相交,那么我们将在交点之前的部分沿 y = -1 翻转,转化成为从 (0, -2) 走到 (n, 0) 的方案数,为
。因此最后我们有
。
在别人的博客上看到几个练习题,先放在这
- https://www.luogu.org/problemnew/show/P1641
- https://loj.ac/problem/10238
- https://www.luogu.org/problemnew/show/P2532
- https://www.luogu.org/problemnew/show/P3200
洛谷P1928 外星密码
这道题代码的写法稍微有些惊艳到了我,因为我是写不出来这个。思路很易懂:对于每一个左方括号都意味着进入下一层递归,待所有的都处理完后(以遇到右括号为中止条件)就把字符串返回。这是经典的递归分治思想:不管方括号内部的运算细节,只当 s 就是方括号内最终的结果。这段代码也涉及到了很多 C++ 实用的东西:比如 string 的加号表示字符串连接,scanf 返回的是成功输入的元素个数(为 EOF 就表示输入终止),整数的读入会自动省略掉后面的非法字符。
string work() {
string temp;
char c;
while (scanf("%c", &c) != EOF) {
if (c == ']') break;
else if (c == '[') {
int d;
scanf("%d", &d);
string s = work();
while (d--) temp += s;
}
else {
temp += c;
}
}
return temp;
}
分治
当我们解决一些较大的问题时,我们可以将这个问题分成几个较小的子问题,分别解决之后,使用得到的信息求解较大问题的答案。我们可以不断递归下去,直到问题的规模足够小后直接解决。假设原问题的规模为 n,我们每次将问题的规模削减一半,那么会分成两个规模为 的子问题,继而分成四个规模为
的子问题...... 在
次后问题的规模会变成常数级别。因此,假如说处理子问题并合并的复杂度为
,那么分治的复杂度为
。假如说处理子问题并合并的复杂度为
,那么分治的复杂度为
。
洛谷P1226 【模板】快速幂
如果我们可以知道 ,那么我们就可以轻而易举地合并出
,代码如下:
long long pow(long long now) {
if (now == 0) return 1;
if (now == 1) return b;
if (now == 2) return b * b % k;
if (now == 3) return (b * b % k) * b % k;
if (now == 4) return ((b * b % k) * b % k) * b % k;
long long tmp = pow(now / 2) % k;
tmp = (tmp * tmp) % k;
if (now & 1) return (tmp * b) % k;
else return tmp;
}
多加几个 now 的判断可以缩短运行时间。
洛谷P1115 最大子段和
我们在序列中任意选一个元素,那么最大的子段要么在这个元素左面,要么在这个元素右面,要么包括这个元素。我们的递归分治即可以此为基础:对于前两种情况,它们是规模更小的子问题;而对于最后一种情况,我们只需要求出以该点为一个端点的最大子段和即可得到答案,最后我们对三个答案取最大值。该算法的时间复杂度为 。
int solve(int left, int right) {
if (right - left == 1) return num[left];
int mid = (left + right) / 2, sum = 0, tmp1 = INT_MIN, tmp2 = INT_MIN, d = max(solve(left, mid), solve(mid, right));
for (int i = mid; i < right; ++i) {
sum += num[i];
tmp1 = max(sum, tmp1);
}
sum = 0;
for (int i = mid - 1; i >= left; --i) {
sum += num[i];
tmp2 = max(sum, tmp2);
}
return max(d, tmp1 + tmp2);
}
这个问题也有 的算法,仅需扫描一遍数组即可。
为“以第 i 个元素结尾的最大子段和”,最终
即为答案。
f[0] = num[0];
for (int i = 0; i < n; ++i) {
f[i] = num[i] + max(0, f[i - 1]);
}
int maxn = f[0];
for (int i = 1; i < n; ++i) maxn = max(maxn, f[i]);
我们甚至可以用滚动数组优化使它仅使用常数空间。在这里我们换了一种写法,大体思路是:使用 sum 记录前缀和并实时维护最大值,如果 sum 变成了负数,那么此时对于下一个元素,我们只应该选择它本身(选择前面的会使结果变小),于是我们将 sum 设置为 0 重新开始。
int n, maxn, sum, d;
scanf("%d%d", &n, &maxn);
sum = maxn;
while (--n){
scanf("%d", &d);
sum = sum > 0 ? sum : 0;
sum += d;
maxn = maxn > sum ? maxn : sum;
}
洛谷P1257 平面上的最接近点对 和 洛谷P1429 平面最近点对(加强版)
fabs 是对浮点数类型的绝对值运算,因为 abs 函数的参数类型是 int。定义 cmp 函数时里面写的是小于号。
#include<bits/stdc++.h>
using namespace std;
double INF;
struct Point{
double x, y;
};
Point point[200005];
double dist(Point a, Point b) {
return sqrt((a.x - b.x) * (a.x - b.x) + (a.y - b.y) * (a.y - b.y));
}
bool cmpx(const Point &a, const Point &b) {
return a.x < b.x;
}
bool cmpy(const Point &a, const Point &b) {
return a.y < b.y;
}
double solve(int left, int right) {
if (right - left == 1) return INF;
if (right - left == 2) return dist(point[left], point[left + 1]);
int mid = (left + right) / 2, k = 0;
static Point temp[200005];
double minn = min(solve(left, mid), solve(mid, right));
for (int i = left; i < right; ++i) if (fabs(point[i].x - point[mid].x) <= minn) temp[k++] = point[i];
sort(temp, temp + k, cmpy);
for (int i = 0; i < k; ++i) {
for (int j = i + 1; j < k; ++j) {
if (temp[j].y - temp[i].y > minn) break;
minn = min(minn, dist(temp[i], temp[j]));
}
}
return minn;
}
int main() {
int n;
scanf("%d", &n);
for (int i = 0; i < n; ++i) {
scanf("%lf%lf", &point[i].x, &point[i].y);
}
INF = dist(point[0], point[1]);
sort(point, point + n, cmpx);
printf("%.4lf\n", solve(0, n));
return 0;
}
CodeForces343C Painting Fence
这题我一开始想复杂了,试图把“横着涂”和“竖着涂”两种情况交替纳入考虑,但是没法通过所有的点,至今不懂为什么。
这也提醒我不要把一开始的想法就作为算法的一部分,而是将他们作为题目的性质纳入考虑,最终结合所有的性质得出一个步骤详尽的算法。
回到这个题目上来,对于一个高度为 h 的竖列,要么我们直接从上往下涂一道将这个竖列涂色,要么我们横向涂抹 h 次。
#include<bits/stdc++.h>
using namespace std;
int n, num[5005];
bool is_painted[5005];
int solve(int left, int right, int level) {
if (right - left == 1) return 1;
int minn = num[left], ans = 0;
for (int i = left + 1; i < right; ++i) minn = min(minn, num[i]);
ans += minn - level;
level = minn;
for (int i = left; i < right; ++i) {
if (num[i] == level) is_painted[i] = true;
}
for (int pos = left; pos < right; ++pos){
if (is_painted[pos]) continue;
int tmp = pos + 1;
for (; num[tmp] > level && tmp < right; ++tmp);
while (num[tmp - 1] == level) --tmp;
ans += solve(pos, tmp, level);
pos = tmp;
}
return min(right - left, ans);
}
int main() {
scanf("%d", &n);
for (int i = 0; i < n; ++i) {
scanf("%d", &num[i]);
}
printf("%d\n", solve(0, n, 0));
return 0;
}