贪心总结(一)
一、简单贪心:
这种贪心就是靠直觉,没有什么套路,唯手熟尔,但是也很好证明它是对的。
1、codeforces1334B Middle Class
题意:
给出$n$个数,你可以选出一个子集然后这个这个子集的数的的值改成它们的平均值,求平均值大于等于$k$的最大子集。
题解:
这些数降序排序,然后预处理前缀和,从前往后找,找到最右边的位置,满足$sum[pos]/pos>=k$即可,$pos$就是答案。因为这个是降序排序的,所以它们的平均值一定是递减的,这样子,我们找到最小的大于$k$的平均值,这样子就一定是最大的子集。
AC代码:
#include <bits/stdc++.h> using namespace std; typedef long long ll; const int N = 1e5 + 5; int a[N]; ll sum[N]; void solve() { int n; ll m; scanf("%d%lld", &n, &m); for (int i = 1; i <= n; ++i) scanf("%d", &a[i]); sort(a + 1, a + n + 1, greater<int>()); for (int i = 1; i <= n; ++i) sum[i] = sum[i - 1] + a[i]; ll maxn = 1; for (; maxn <= n && sum[maxn] / maxn >= m; ++maxn) ; printf("%lld\n", maxn - 1); } int main() { int T; scanf("%d", &T); while (T--) solve(); return 0; }
2、codeforces1324C Frog Jumps
题意:
青蛙跳过一段河,河上有一些木板,木板上能够跳跃的方向指定,青蛙可以跳任意长度,求它通过河的最小的最大长度。
题解:
初看以为是二分答案(实际上也行,时间复杂度$O(nlogn)$),但是细看我们可以发现,青蛙跳到只能往左跳的木板时,它可以只往左跳一格,直到跳到往右跳的木板上。或者到起点,所以实际上答案就是最长的相邻两个向右的木板的距离。
AC代码:
#include <bits/stdc++.h> using namespace std; const int N = 2e5+ 5; typedef long long ll; char s[N]; int minp[N]; void solve() { memset(minp, 0, sizeof(minp)); scanf("%s", s + 1); int n = strlen(s + 1); int prev = 0, maxn = 0; for (int i = 1; i <= n; ++i) if (s[i] == 'R') maxn = max(maxn, i - prev), prev = i; maxn = max(maxn, n + 1 - prev); printf("%d\n", maxn); } int main() { int T; scanf("%d", &T); while (T--) solve(); return 0; }
3、codeforces1305B Kuroni and Simple Strings
题意:
给出只含有$‘(’$和$')'$的字符串,一次操作可以去掉一个匹配串,求多少次可以使这个串不再匹配或者空。
题解:
首先我们可以手推几个例子,如$(()())$(样例二),我们可以删去$1,2,5,6$,这样子就满足题意了,在这个答案里,左括号的位置小于右括号。又如$(()()())$,我们发现可以删去$1,2,4,5,7,8$,这样子也可以满足。因此我们觉得只需要从两端对应删除就好,真的是这样吗?随意给出一个字符串$()())())((()()((()()()$,我么发现删除$1,3,6,9,10,12,14,18,20,22$也可以。
现在给出证明,不能匹配的串中,右括号一定在左括号的前面,否则我们一定可以找到一对括号把它删除,那么我们确实只需要从两边碰到一对就删一对,直到相遇,这样子剩下的右括号一定会在左括号的前面。
AC代码:(我当时是觉得会有好几步,所以写了$vector<vector<int>>$,实际上只可能是$0$或者$1$)
#include <bits/stdc++.h> using namespace std; typedef long long ll; const int N = 1e3 + 5; char s[N]; bool f[N]; vector<int> pre, suf; vector<vector<int>> v; void solve() { scanf("%s", s + 1); int n = strlen(s + 1); int ans = 0; while (1) { pre.clear(); suf.clear(); for (int i = 1; i <= n; ++i) if (!f[i] && s[i] == '(') pre.push_back(i), f[i] = 1; for (int i = n; i >= 1; --i) if (!f[i] && s[i] == ')') suf.push_back(i), f[i] = 1; int cnt = 0; while (cnt < min(pre.size(), suf.size()) && pre[cnt] < suf[cnt]) ++cnt; if (!pre.size() || !suf.size() || !cnt) break; v.push_back(vector<int>()); for (int i = 0; i < cnt; ++i) v[ans].push_back(pre[i]); for (int i = cnt - 1; i >= 0; --i) v[ans].push_back(suf[i]); ++ans; } printf("%d\n", v.size()); for (auto &i : v) { printf("%d\n", i.size()); for (int j = 0; j < i.size(); ++j) printf("%d%c", i[j], " \n"[j == i.size()]); } } int main() { int t = 1; //scanf("%d", &t); while (t--) solve(); return 0; }
4、codeforces1338B/1339D Edge Weight Assignment
题意:
给出一棵无根树,现在让你给边赋权值,要求叶子结点之间形成的路径的异或都是$0$,问你这些权值中不同的值最少有多少种,最多有多少种。
题解:
这个题看起来有点困难,因为我们看到了路径,因为路径相关的题大部分都是树剖或者树形$dp$。但是这个题就不太一样。首先我们看,一条路径如果长度是偶数,则其只要权值都一样,异或和就一定是$0$,如果是奇数,那就需要至少$3$个数,比如说$100001_2,100000_2,000001_2$。所以我们先把这棵树转成有根树,然后预处理出深度和叶子结点,然后求路径长度就是就是求$lca$,然后$dis(u,v)=dis(u,lca)+dis(lca,v)$,但是这里有个小$trick$:因为到达$lca$之后,再往上到达其父节点,相当于两个都往上走,这样子经过的边的数量必定是偶数,不会对结果有影响,所以可以直接求它们到根的距离。这样子,只要全是偶数或者全是奇数,就输出最小值$1$,否则最小值是$3$。对于最大值,我们发现,如果是同一个父亲的叶子结点,它们的边权一定是一样的。对于其他的边,自然是可以随意放。所以我们只需要处理出每个点上的叶子儿子有多少就可以了。
AC代码:
#include <bits/stdc++.h> using namespace std; typedef long long ll; const int N = 1e5 + 5; vector<int> G[N]; vector<int> le; int dep[N]; void dfs(int u, int fa) { dep[u] = dep[fa] + 1; for (auto i : G[u]) if (!dep[i]) dfs(i, u); } void solve() { int n; scanf("%d", &n); for (int i = 1; i < n; ++i) { int u, v; scanf("%d%d", &u, &v); G[u].push_back(v); G[v].push_back(u); } dfs(1, 0); //树根深度为1 for (int i = 1; i <= n; ++i) if (G[i].size() == 1) le.push_back(i); int ecnt = 0; for (int i = 0; i < le.size(); ++i) if (dep[le[i]] % 2 == 1) ++ecnt; if (ecnt == le.size() || ecnt == 0) printf("1 "); else printf("3 "); int cnt = n - 1; for (int i = 1; i <= n; ++i) { int tot = 0; for (auto j : G[i]) tot += (G[j].size() == 1); if (tot >= 2) cnt -= tot - 1; } printf("%d\n", cnt); } int main() { int T = 1; //scanf("%d", &T); while (T--) solve(); return 0; }
5、codeforces1304D Shortest and Longest LIS
题意:
给出长度是$n-1$的一个由$'<'$和$'>'$构成的串,构造出一个长度是$n$的,由数字$1~n$构成的序列,且大小关系满足给出的串,输出$LIS$最短和最长时的序列。
题解:
最大的时候,首先我们先把这些数升序排列,然后遇到$'>'$,我们就把和这些大于号相关的数直接逆序,这样子一定能构造出最长的$LIS$,最短的同理,先降序排列,然后遇到$'<'$,我们也直接逆序,然后就是结果。
具体的解法和AC代码见:codeforces1304D。
6、codeforces1301C Ayoub's function
题意:
求长度为$n$包含$k$个$'1'$的二进制串的所有情况中包含$'1'$的子串最大值。
题解:
这个正着太难想了,情况非常多,我们只能反着想,考虑最少的全0的子串,我们知道,$a^2+b^2<(a+b)^2$,所以每一段全为0的子串的长度尽可能平均就是最好的。
AC代码:
#include <bits/stdc++.h> using namespace std; const int N = 1e5 + 5; typedef long long ll; ll getnum(ll n) { return n * (n + 1) / 2; } int solve() { ll n, m; scanf("%lld%lld", &n, &m); ll z = n - m, di = z / (m + 1), le = z % (m + 1); ll ans = getnum(n); ans -= (m + 1ll - le) * getnum(di) + le * getnum(di + 1); printf("%lld\n", ans); return 0; } int main() { int t; scanf("%d", &t); while (t--) solve(); return 0; }
7、Gym101986C Medical Checkup
题意:
给出每个人做任务的消耗时间,做每一项任务的时间都相同,任务数量无限,求出在某个时刻后,这个人在做哪一项任务。
题解:
比赛的时候暴力推了所有人的开始时间和结束时间,开始时间找到了复杂的规律,这样子写程序会非常复杂容易出错,但是我们可以注意到结束时间,除了完成第一个任务要等之外,结束时间一定是某个人之前的最大的时间,因为短的时间一定要等长时间的做完。
AC代码:
#include <bits/stdc++.h> using namespace std; typedef long long ll; const int N = 1e5 + 5; ll sum[N]; ll a[N]; int main() { int n, t; scanf("%d%d", &n, &t); for (int i = 1; i <= n; ++i) scanf("%lld", &a[i]), sum[i] = sum[i - 1] + a[i]; int maxn = 0; for (int i = 1; i <= n; ++i) { if (a[i] > maxn) maxn = a[i]; if (t >= sum[i]) printf("%lld\n", (t - sum[i]) / maxn + 2); else printf("%lld\n", 1ll); } return 0; }
※ 参考《算法竞赛进阶指南》
贪心法的考虑和证明如下:
微扰法:
交换邻项时或者使用邻项为答案时,会使得决策变差,这个用于排序贪心题,如上第$1$
放缩法:
这么决策的作用范围的扩展不会使整体结果变差。(慢慢体会)
决策包容性:
这个决策的目标状态包含了所有的其他到了这个状态的决策(否则可能有更优的)
反证法和数学归纳法(后面会提到)
$To$ $be$ $continued$