枚举专题(分析 思路 证明)

枚举专题题解(普及-以上)

在我看来 枚举是将复杂问题简单化的一种方式

下面就请大家来跟我一起领悟枚举的魅力


p2141珠心算

这题很像leetcode 13.三数之和

题目

珠心算是一种通过在脑中模拟算盘变化来完成快速运算的一种计算技术。珠心算训练,既能够开发智力,又能够为日常生活带来很多便利,因而在很多学校得到普及。

某学校的珠心算老师采用一种快速考察珠心算加法能力的测验方法。他随机生成一个正整数集合,集合中的数各不相同,然后要求学生回答:其中有多少个数,恰好等于集合中另外两个(不同的)数之和?

最近老师出了一些测验题,请你帮忙求出答案。

题意
大意是 选择三个数使得两个数的和等于另一个数

这种题目很明显就是能靠排序来优化时间复杂度的
(经验之谈, 若排序不会对数据的性质影响, 那么试试排序后再来思考此题)

排序后 我们可以发现 i, j 一定是在 k 之前的 (k为和数)
我们不妨设i < j 由双指针思想我们可以得到这样的简化

 while (l < r) {
            if (q[l] + q[r] < tar) l ++;
            else if (q[l] + q[r] > tar) r --;
            else {
                res ++;
                break;
            }
        }

解释一下 如果 当前两个数之和大于tar我们就移动右指针否者移动左指针
相比于暴力n ** 2 这样搜索的时间复杂度是 n的 效率得到了大大的提高
那么问题来了 为什么可以这样搜索 不会漏数据吗?
由于数组是有序的 那么i的左边一定会<=当前i 同理 j的右边一定>=当前j
如果想要i + j == tar 那么当总和大于 tar的时候一定得去移动右指针使得总和变小相反同理,所以我们每次的搜索都是向着可能的一步,遍历的也是可能的数据,所以得到的结果是正确的。

到这里大家都知道这题该怎么做了吧?

思路:

反向遍历数组 将该数当作tar 然后在其后面的区间进行搜索
时间复杂度 O(n ** 2)

代码

#include <iostream>

using namespace std;

const int N = 10010;

int q[N];
int n;

void quick(int l, int r) {
    if (l >= r) return;
    int x = q[l + r >> 1];
    int i = l - 1, j = r + 1;
    while (i < j) {
        do ++ i; while(q[i] < x);
        do -- j; while (q[j] > x);
        if (i < j) swap(q[i], q[j]);
    }
    quick(l, j), quick(j + 1, r);
}

int main() {
    cin >> n;
    for (int i = 1; i <= n; ++ i) cin >> q[i];
    int res = 0;
    quick(1, n); // 排序
    for (int end = n; end > 2; -- end) {
        int l = 1, r = end - 1;
        int tar = q[end];
        while (l < r) {
            if (q[l] + q[r] < tar) l ++; // 如果过大 那么 移动左指针
            else if (q[l] + q[r] > tar) r --; // 反之移动右指针
            else {
                res ++;
                break;
            }
        }
    }
    cout << res << endl;
    return 0;
}

p1147连续自然数和

题目

对一个给定的自然数M,
求出所有的连续的自然数段(每一段至少有两个数),
这些连续的自然数段中的全部数之和为M。

这是一个很典型的滑动窗口了

思路

每次维护窗口里的和, 使得 sum <= Msum == M时 输出结果并继续

证明

r 能与前面某个 l 组成答案 那么在r - 1的时候 sum 一定是小于 M
所以每个答案都会被正确的选上(也就是当选到正确的r时正确的l不会被弹走);

代码

时间复杂度O(n)

#include <iostream>

using namespace std;

const int N = 2000010;
typedef unsigned long long ULL;

ULL q[N];
int n;

int main() {
	cin >> n;
	for (int i = 1; i <= n; ++ i) {
		q[i] = i;
	}
	int l = 1, r = 1; // 维护 [l, r] 之间的窗口
	int sum = 0;
	for (; r < n; ++ r) {
		sum += r;
		while (sum > n) sum -= q[l ++]; // 若窗口内数据过大 移动右指针
		if (sum == n) printf("%d %d\n", l, r);
	}
	return 0;
}

p8672交换次数

这是能做的题里面等级最难的(唯一的一道蓝题没权限打开)
但是 让我们用枚举来让无头绪的问题变得简单吧!!!

分析

我们需要将B A T 三位dalao的座位连在一起, 直接看会感到十分迷惑, 如何去寻找这个最小次数?
但是 我们发现排完后的座位无非就6种(A(3, 3))所以我们可以直接去枚举所有的方式 这样问题就简化为了如何用最少的方法实现 某一种排序 由简单的贪心思想可以想到如果我们这一步能同时使两位的位置都正确,那么我们一定要移动这一步,这样就可以使得总移动次数最少 所以我们需要处理出来每次有多少个位置能被同时换到正确的位置,由于只有三种位置所以不难想到只需要考虑前两种位置,那么我们分别计算出每个位置有多少个数需要去到对应的位置(例如 f12表示1区域要需要去到2区域的数有多少个)所以交换次数就是 min(f12, f21) + abs(f12 - f21) + f13 + f31 我们遍历6个位置取最小值即可

证明

贪心思路即使证明

如果我们这一步能同时使两位的位置都正确,那么我们一定要移动这一步,这样就可以使得总移动次数最少

代码

O(n)

#include <iostream>
#include <string>
#include <vector>
#include <cmath>

using namespace std;

const int N = 100010;

string s;
int b, a, t;
typedef pair<char, char> PCC;

char c[6][2]{ 'B', 'A', 'A', 'B', 'A', 'T', 'T', 'A', 'B', 'T', 'T', 'B' };

int f(vector<int>& v, int x) {
	int f12 = 0, f13 = 0;
	int f21 = 0, f23 = 0;
	char s1 = c[x][0], s2 = c[x][1];
	for (int i = 0; i < v[0]; ++ i)   /··········
		if (s[i] == s2) f12++; 
		else if (s[i] != s1 && s[i] != s2) f13++;
	for (int i = v[0]; i < v[1] + v[0]; ++i)//        取得f12 等数据
		if (s[i] == s1) f21++;
		else if (s[i] != s1 && s[i] != s2) f23++;       //···········
	return min(f12, f21) + f13 + f23 + abs(f12 - f21);
}

int main() {
	cin >> s;
	int res = 0;
	int ans = 0x3f3f3f3f;

	for (auto& e : s) {
		if (e == 'B') b++;
		else if (e == 'A') a++;
		else t++;
	}
	vector<vector<int>> v;
	v.push_back({ b, a });
	v.push_back({ a, b });
	v.push_back({ a, t });
	v.push_back({ t, a });
	v.push_back({ b, t });
	v.push_back({ t, b });
	for (int i = 0; i < 6; ++i) {
		ans = min(ans, f(v[i], i));
	}
	cout << ans << endl;
	return 0;
}

p1618三连击

三连击就是一个纯纯暴力了

思路

枚举符合第一个数然后通过比例算出后面的两个数 最后进行检查

代码

#include <iostream>
#include <cstring>

using namespace std;

double n1, n2, n3;

bool cal(double a, double b, double c) {
	bool st[10];
	memset(st, 0, sizeof st);
	st[0] = true;
	int res = 0;
	int x[3] = { a, b, c };
	for (int i = 0; i < 3; ++ i) 
	while (x[i]) {
		if (st[x[i] % 10]) return false;
		st[x[i] % 10] = true;
		x[i] /= 10;
	}
	return true;
}

int main() {
	cin >> n1 >> n2 >> n3;
	if (n1 == 0) {
		puts("No!!!");
		return 0;
	}
	double a = -1;
	bool flag = true;
	for (int i = 1; i <= 9; ++ i) 
		for (int j = 1; j <= 9; ++ j)
			for (int k = 1; k <= 9; ++ k) 
				if (i != j  && i != k && j != k) {
					a = i * 100 + j * 10 + k;
					if (a < 100) continue;
					double b = n2 / n1 * a, c = n3 / n1 * a;
					if (cal(a, b, c)) {
						printf("%.0f %.0f %.0f\n", a, b, c);
						flag = false;
					}
				}
	if (flag) puts("No!!!");
	return 0;
}

P2241 统计方形

对数学有敏感的可以很容易发现这题其实是是一道数学题

那么加下来我们直接一步一步推公式

思路

对于正方形:

回想我们人类是如何数正方形的
先数 1 x 1 然后 2 x 2 最后 ...
我们把这个思路总结成公式就是n * m + (n - 1) * (m - 1) + ....直到边长x == min(n, m)

对于长方形:
我们直接去求会显得很困难(当然也不是不行)
但是所有四边形的数量是可以求出来的, 那么我们是否可以通过四边形 - 正方形 == 长方形??? 这里我们简单分析一下就可以发现是可以的
那么问题转化为了怎么取求四边形的数量
先从小问题开始想 对于一个1行3列的图像它包含了多少个长方形数量?
还是一样 我们以人类的角度来分析这题 答案应该是n + n - 1 + n - 2 + ... + 0
也就是(1 + n) * n / 2个 可是这只是一行的啊。
接下来扩展到n行:
对于这个一维的数据我们要扩展到二维也就是需要给他加上宽的属性,同理枚举他的宽 (1 + m) * m / 2
(如果无法理解 可以想象一个n * m 的四边形x不变y不断变大的过程)

..######
..######
########
########
########
->
########
..######
..######
########
########
->
########
########
..######
..######
########
->
########
########
########
..######
..######

代码

O(min(n, m))
#include <iostream>

using namespace std;
typedef unsigned long long ULL;

int main() {
	int n, m;
	cin >> n >> m;
	ULL res1 = 0;
	for (int i = 0; i <= min(n, m) - 1; ++i) {
		res1 += (n - i) * (m - i);
	}
	ULL nums1 = (1 + m) * m / 2;
	ULL mm = (1 + n) * n / 2;
	ULL sum = nums1 * mm;
	cout << res1 << " " << sum - res1 << endl;
	return 0;
}

P2010 回文日期

分析

由于回文串的对称性, 那么确定了年份就可以确定月日, 反之同理
那么这题我们就有两种枚举方式

  1. 枚举年份 判断月份是否合法
  2. 枚举月份判断年份是否合法
    综合一下发现2方法会更好处理(少了对月日合理性的判断)
    且效率更快, 所以我们采用2方法
思路

枚举出每一个月的每一天,然后生成对应时期, 最后进行判断

代码
O(1)

#include <iostream>
#include <string>
#include <algorithm>

using namespace std;

int main() {
	string d1, d2;
	int data[13] = { 0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 };
	cin >> d1 >> d2;
	int res = 0;
	for (int i = 1; i <= 12; ++i) {
		for (int j = 1; j <= data[i]; ++j) {
			string s;
			if (i < 10) s += '0';
			s += to_string(i);
			if (j < 10) s += '0';
			s += to_string(j);
			string temp = s;
			reverse(temp.begin(), temp.end());
			temp += s;
			string temp1;
			temp1 = temp;
			reverse(temp.begin(), temp.end());
			if (temp1 == temp && temp1 <= d2 && temp1 >= d1) res++;
		}
	}
	if ("92200229" >= d1 && "92200229" <= d2) res++; // 这里是特判0229
	cout << res << endl;
	return 0;
}

P8635 四平方和

分析

很快的我们会想到 4重循环 枚举每一种可能
保留字典序最小的一个即可
可惜的是数据范围是 10 ** 6 时间限制是3s 我们这个暴力枚举的时间复杂度是O(n ** 2的 严重超时

那么我们想着优化一下 如果我们先枚举出一个数,再去枚举另外三个数
是不是时间复杂度就降为O(nlogn)了?

既然可以这样优化那为什么不平分呢?显然平分是效率最高的枚举方式

所以我们想到先枚举后两位 再去枚举前两位O(n) (hash表的插入近似为O(1))

思路

我们枚举后两位的所以可能,将其结果存入hash表中,再枚举前两位,当枚举到
第一个可能的组合 即为答案

实现的细节见代码注释

代码
O(n)
#include <iostream>

using namespace std;

const int N = 5 * 10000010;

int l[N], r[N];
bool st[N];

int main() {
	int n;
	cin >> n;
	for (int i = 0; i * i <= n; ++ i) {
		for (int j = i; j * j + i * i <= n; ++j) {
			int x = i * i + j * j;
			if (!st[x]) {     // 为了保证组成 x 的两个数字典序最小
				st[x] = true;
				l[x] = i;
				r[x] = j;
			}
		}
	}

	for (int i = 0; i * i <= n; ++i) {
		for (int j = i; j * j + i * i <= n; ++j) {
			int x = i * i + j * j;
			int y = n - x;
			if (st[y]) { // 如果这个数在上一次出现过, 也就是后两个数能拼出这个 `y`
				printf("%d %d %d %d", i, j, l[y], r[y]);
				return 0;
			}
		}
	}
	return 0;
}
证明

没想到吧, 我把证明写在后面,为什么呢, 我想看看有没有细心的同学发现了一些不对劲的地方。。。 可以回去看看再回来
























ok, 我们继续 细心的同学可能会想为什么可以直接输出(ij, l[y], r[y])题目不是要求排序吗?

很好, 我们做题就要有这种精神——每一个题的做法一定要完全弄懂,而不是只是会个做法就结束 以AC为最终目的的刷题效率是很低的, 这也是为什么我拼了命的也要给几乎每个题写证明, 因为平时多思考, 多想,需要用的时候才能快速的反应

废话不多说 为什么?

假如四个数为 i j k z
由枚举顺序我们可以很容易的得到 i <= j && k <= z 但是如何证明 j <= k
我们不妨利用反证法:
设: 我们输出的答案中 j > k
我们知道 tar == i * i + j * j + k * k + z * z 是无关顺序的, 也就是我们可以将i * ij * j的顺序反过来, 既然k < j 那么我们在枚举前两个数的时候一定会先枚举到i 与 k 那么去后面找有没有j * j + k * k时是一定能找到的,而如果我们输出的是 i, j, k, z那么一定是先枚举到了i 与 j得出 j < k所以假设不成立


P1271 选举学生会

快排裸题 不细讲

代码
O(nlogn)
#include <iostream>

using namespace std;

const int N = 2000010;

int q[N];

void qs(int q[], int l, int r) {
    if (l >= r) return;
    int x = q[l + r >> 1];
    int i = l - 1, j = r + 1;
    while (i < j) {
        do ++ i; while (q[i] < x);
        do -- j; while (q[j] > x);
        if (i < j) swap(q[i], q[j]);
    }
    qs(q, l, j), qs(q, j + 1, r);
}

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

P3654 First Step

分析

偶像们的站法无非就两种 1.竖着 2.横着

所以我们只需要找到横着和竖着各有多少种方法即可

思路

明显的在k个连续的地方选连续的j个点的选法有k - j + 1种(k >= j)
所以 我们只需要计算出每一个在一条直线上连续的空地即可

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

using namespace std;

const int N = 110;

char g[N][N];
bool st[N][N];
int n, m, k;

int cal(int x, int y, int type) {
	st[x][y] = true;
	int i = 0;
	if (type == 0) {
		for (i = y; i <= m && g[x][i] == '.'; ++i) st[x][i] = true;
		return i - y;
	}
	else {
		for (i = x; i <= n && g[i][y] == '.'; ++i) st[i][y] = true;
		return i - x;
	}
}

int main() {
	cin >> n >> m >> k;
	int cc = 0;
	for (int i = 1; i <= n; ++i) {
		for (int j = 1; j <= m; ++j) {
			cin >> g[i][j];
			if (g[i][j] == '.') cc++;
		}
	}
	int res = 0;
	for (int i = 1; i <= n; ++i) {
		for (int j = 1; j <= m; ++j) {
			if (g[i][j] == '.' && !st[i][j]) {
				int c = cal(i, j, 0);
				if (c >= k)
					res += c - k + 1;
			}
		}
	}
	memset(st, 0, sizeof st);
	for (int i = 1; i <= n; ++i) {
		for (int j = 1; j <= m; ++j) {
			if (g[i][j] == '.' && !st[i][j]) {
				int c = cal(i, j, 1);
				if (c >= k)
					res += c - k + 1;
			}
		}
	}
	if (k > 1) cout << res << endl;  // 特殊处理下 由于k -- 1 时 横竖状态相等
	else cout << cc << endl;
	return 0;
}

P1149 火柴棒等式

终于到我们的最后一题了, 一次写这么多真的好累啊,hh, 我现在我自难受得不行;

分析

首先用于拼数的火柴一定只有 n - 4根(减去+ =
那么我们来算一下数据范围 1111 + 0 = 1111 要 26根已近超过了 所以我们需要枚举的数据范围就在1111的附近, 为了准确性, 我们直接多枚举几位然后计算答案个数即可

思路

预处理出1 - n >= 1211的所有数据需要的火柴 最后进行计算

代码
#include <iostream>
#include <map>
#include <string>

using namespace std;

const int N = 5000;

int op[N];
int m[10] = { 6, 2, 5, 5, 4, 5, 6, 3, 7, 6 };

int cal(const string& s) {
	int sum = 0;
	for (auto e : s) {
		sum += m[e - '0'];
	}
	return sum;
}

int main() {
	int n;
	cin >> n;
	for (int i = 0; i <= 2232; ++i) {
		op[i] = cal(to_string(i));
	}
	int res = 0;
	for (int i = 0; i <= 1111; ++i) {
		for (int j = 0; j <= 1111; ++j) {
			if (op[i] + op[j] + op[i + j] == n - 4) {
				res++;
			}
		}
	}
	cout << res << endl;
	return 0;
}

ok, 本次枚举专题就到这里啦!!!完结撒花!!!

若有什么问题欢迎随时提问

有一说一, 写这么一篇博客是非常花费时间的, 我已经在电脑前连续坐了3个小时了,hh, 希望能对大家有所帮助。

评论 6
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值