贪心专题
1. [NOIP2015 普及组] 推销员
link:https://www.luogu.com.cn/problem/P2672
思路跟正解大差不差,但想的有点复杂了。先把所有的按疲劳值排个序,(这样省却了找最大疲劳值的过程),然后只用考虑第x大的和后面距离+疲劳值最大值的比较即可(累了,不想写了)
2. Two Heaps
link:https://www.luogu.com.cn/problem/CF353B
思路一开始想到了一些,就是把相同的数平均分到两堆里,但如果有多个数出现次数不同,最好是将大的放在后面,这样子就可以保持交替均分,避免有这种情况:
12 12 12 13 14 14 14 15
→ 12 12 14 14
分到一个堆里,因为那个13夹在中间。
排序写得有点不一样:
点击查看代码
bool cmp(node a,node b)
{if(cnt[a.val]==cnt[b.val])return a.val<b.val;return cnt[a.val]<cnt[b.val];
}
以上就可以做到既可以排序(找相同的数)又能让出现多的数放的最后了。
3. Antichain
link:https://www.luogu.com.cn/problem/CF353E
感觉紫评的有点过了,蓝题还差不多。
任意两个点互相达不到说明这两个点之间不存在一条链(链的两个端点就是这两个点),使得这条链是同向的。显然这个图里有很多条这样的链,肯定取每一条链除了端点外的任意一个节点(总个数就是链的条数),至于端点,其实和链里面的点是一样的,如果这个端点左右两边都是端点且左右都没有取,那么这个点就可以被取。最后两个个数之和就是答案。代码也挺好写的,不知道为啥题解里写的那么长。
4. [AGC057A] Antichain of Integer Strings
link:https://www.luogu.com.cn/problem/AT_agc057_a
此题让我对二分的单调性多了一层理解。一开始还读错题了(bushi),以为只是后缀就行,想+写了半个小时才发现不对。这个题目上的样例解释一定不要看,误导了我半天。首先最后这个集合一定是一段连续的值,这个证明想一想就出来了,然后会发现每一个数的第一个比它大的包含它的数为 \(f(x)=min(10x, x+10^{len(x)})\) ,那么 f(x) 一定要大于 r 才能选进来。发现 f(x) 其实是有单调性的,二分出最小的x即可。
5. [USACO10MAR] Barn Allocation G
link:https://www.luogu.com.cn/problem/P1937
很容易得想到线段树维护区间加和区间最值,但始终想不出贪心策略。看了题解后理解了,大概就是两个策略:
- 右端点相同时,按照左端点从大到小的顺序判断。这个比较好理解,因为较大一点的区间代价也就更多,这样子就会使得较小一点的区间难以取到,不优。
- 按照右端点从小到大的顺序判断。题解里说的有点点复杂,感性理解一下,我们希望的是让区间以从前往后的顺序排列(以从后往前的顺序也行,只不过所有策略都是相反的,道理一样),这样子能使得前后区间互相影响得最小。一开始我在想为什么不以左端点从小到大排列,原因是整体是一个从前往后的趋势,如果有左端点靠左、右端点很靠右的区间,就很容易影响到后面,所以是按照右端点来排序的。
6. [USACO09OCT] Allowance G
link:https://www.luogu.com.cn/problem/P2376
每一个面额都能整除所有比它大的面额。这句话一定要仔细想想。会发现大面值的都可以被小面值的凑出来,所以小面值产生的浪费一定比大面值的少(例如:6<=5+5和1+5,1+5更优)。所以在选取的时候,应先选大面值的,直到差的钱比当前面值小,再往小了选。这个就比较像八下物理第一章讲的天平一样,砝码是从大往小放。还有一个策略是当前选完后如果还是不够,再从小往大找到第一个 ≥ 差的钱的面值,使得浪费最小。
7. Competition
link:https://www.luogu.com.cn/problem/CF144E
首先需要明确的是每个运动员最终到达的辅助对角线上的点是一个区间(且是最短路),我读题的时候差点读错了。然后这个题我比较纠结的点就是两个运动员不能在同一时间在同一格子里,它让我联想到前几天考试改的T3,但我总不能状态压缩每一个运动员的位置吧。然后就卡住了。
但其实仔细想想(看题解ing)会发现这个问题是可以解决的。两个运动员在同一格子里要么是最终在同一格子里,要么是路径有交叉。后者的解决方案大致就是这个图:
然后最终问题就是每个运动员在区间内选一个点,使得不重复且被选的运动员最多。最优策略为按照左端点从小到大排序,左端点相同时按照右端点从小到大排序。这个策略其实和第5题很像。然后拿堆优化即可(时复O(nlogn)。
8. Jeff and Permutation
link:https://www.luogu.com.cn/problem/CF351E
真的没啥思路。题解很神奇,原来每个数取反与不取反的逆序对数都不受其他数的改变而影响。对于当前数i:
- 取反。前面比他小的数就比他大了 → 前面比他小的数的个数。
- 不取反。后面比他小的数还是比他小 → 后面比他小的数。
这两个值取 min 总共再求和即可。
But, 其实我当时并没有看懂。后来经过探讨并多换了几篇题解才理解,为什么取反和不取反只用考虑一边?为什么不用考虑其他数的正负?其实它省略了一个排序的步骤,从最大的 a[i] 开始:
- 取正:由于这个数是最大的,所以前面的所有数取正或取负都不会大于这个数,也就是说对于前面的数不会产生逆序对。同时,因为这个数是最大的,所以后面的数不管怎么取,都一定比这个数小,对于后面的数就一定会产生 n−i 对逆序对。
- 取负:由于这个数是最大的,所以前面的所有数取正或取负都不会小于这个数,也就是说对于前面的数一定会产生 i−1 逆序对。同时,因为这个数是最大的,所以后面的数不管怎么取,都一定比这个数大,对于后面的数就一定不会产生逆序对。
最大的数考虑完了其实可以删除了,后面计算时完全可以无视比他大的值,每次计算时都可以把它当成最大的数来看。至于排序,因为不影响什么所以就省略了。
9. [HEOI2015] 兔子与樱花
link:https://www.luogu.com.cn/problem/P4107
真的有点不想写了啊啊
还是写吧
自底向上从小到大贪心删节点。证明详见题解。
第10道貌似是基环树不会就跳过去了。这个题单也算是完结了吧。
并查集专项
第一道题太简单就不放了。
1. Tokitsukaze and Two Colorful Tapes
link:https://www.luogu.com.cn/problem/CF1677C
事实证明我现在还不能切掉绿题,我觉得并查集比较不好想到的就是如何建边。此题就是将每一位上的颜色建边,最后必定会形成多个环,我们希望上下差值和最大,就是环上一大一小一大一小放置。会发现最终的答案就为 2*(所有环上较大数的和-所有环上较小数的和) ,因为这些值最终都在 [1,n] ,所以就让大的数为后 n/2 个数,小的数为前 n/2 个数。
2. AND-MEX Walk
link:https://www.luogu.com.cn/problem/CF1659E
神仙题。首先发现这个答案只有0,1,2三种情况。如果出现2,它的末尾为0,怎么&也无法出现1。所以1和2不能同时出现。
-
0的情况:
\(2^{30}\),指的是二进制下30位。再加上题目里的与运算,很容易想到开30个并查集,第 i 个并查集将所有边权第 i 位为1的点连接。因为答案是0,说明至少有一位全部为1,即u与v在至少一个并查集是连通的。 -
1的情况:
稍微复杂一些。答案为1,说明出现了0,但最后一位为0。因为0的情况已经排除了至少有一位全部为1的情况,所以没有任何一位全部为1,所以必定会有0,至于最后一位为0,可以将边权最后一位为0的点与一个虚点0连接(也是开30个并查集),判断u点(除了第一位,否则有可能是1)是否与虚点0联通。如果联通,说明这一位经过了几个1后碰到一个0,即这个边权一定是大于1的,保证了1绝不会出现。 -
2的情况:除了上面两种的都为2。
放一个封装版并查集:
点击查看代码
struct dsu{int fa[maxn];void init(){for(int i=0;i<maxn;i++) fa[i]=i;}int find(int x){return x==fa[x]?x:fa[x]=find(fa[x]);}void merge(int x,int y){fa[find(x)]=find(y);}bool judge(int x,int y){return find(x)==find(y);}
}a[35], b[35];
3. Anton and Tree
link:https://www.luogu.com.cn/problem/CF734E
一开始思路错了,想得太简单。如果把同一个颜色的联通块合并为一团,最优方案为从根到叶子一层一层变色,答案即为树的直径/2。大部分做法都是用并查集合并,其实只需要找到两边颜色不同的端点并把边权赋为1即可。
4. Sanae and Giant Robot
link:https://www.luogu.com.cn/problem/CF1687C
神仙题*2。考虑将题目上的条件转化,\(c[i]=a[i]-b[i]+c[i-1]\),c数组也就是差值的前缀和。当且仅当 \(c[l-1]==c[r]\) 时,区间 [l,r] 可改变,[l,r]这个区间变为 \(c[l-1]\) 。我们最终的目标是让所有c值都为0,所以当 \(c[l-1]==c[r]==0\) 时,改变他才是最优的。相当于选择两端点为0且能操作的区间进行改变,如果最终能使得所有c值都为0即为 YES
,反之为 NO
.
然后考虑这个操作顺序以及如何操作。整体思路很简单,就是将为0的左端点取出来,判断右端点是否为0,然后中间所有数清零,然后不断循环以上操作。需要用到 vector、set、queue
这些STL来维护,就是看码力了。
点击查看代码
//q为值是0的下标,s是值不是0的下标,vec[i]是i的另一个区间端点
while(q.size())
{int i=q.front();q.pop();for(int j:vec[i]){if(c[j]==0){int l=min(i, j), r=max(i, j);set<int>::iterator it;it=s.lower_bound(l);while(it!=s.end()&&*(it)<=r){c[*it]=0;q.push(*it);set<int>::iterator it1=it;it++;s.erase(it1);}}}
}
5. [HEOI2016/TJOI2016] 树
link:https://www.luogu.com.cn/problem/P4092
感觉是后面几道题唯一一道在正常范围之内的。并查集的定义就很显然了,find(i) 就是i的最近的标记的祖先。当u节点被标记时,fa[u]=u,否则,fa[u]=father[u],就是很正常的查找、路径压缩。然后倒序处理每个询问,先将最终状态跑出来,然后就是删除标记的过程。每当一个点删除标记,它的fa值就指向他的父亲。至于为什么不能正序处理,是因为增加标记和删除标记是不同的,删除时,曾经那些答案为当前删除节点的都会随着并查集往上找,而增加是不会改变当前节点儿子的答案的。最后记得正序输出。
感觉自己代码能力很弱肿么办??
6. Nene and the Passing Game
link:https://www.luogu.com.cn/problem/CF1956F
难难难
比较简单的想法就是判断两个人是否能传球并连边,答案为联通块的个数,但时间复杂度为O(n^2)的,考虑优化。
先将题目给的式子转化一下,不妨设 \(i<j\) ,则 \(l_i+l_j<=j-i<=r_i+r_j\),移项并整理可得 $ i+l_i<=j-l_i \ and \ i+r_i>=j-r_i $。设 i 的左区间为 \([i-r_i,i-li]\) ,右区间为 \([i+l_i,i+r_i]\) ,则i和j能传球当且仅当i的右区间和j的左区间香蕉(将满足这一条件的点成为合法的点)。
我们可以建立一些虚点来表示合法的点。将每一个队员可以到达的区间转换为左、右两个端点都是合法的点,将每个队员和他所到达的两个区间的右端点连边,再将右端点和前面到左端点为止的点两两连边。最后再判断有几个联通块。
至于覆盖区间,查找左端点、右端点等操作,可以利用差分的思想,覆盖这一区间相当于给这个区间全部+1,相当于左端点+1,(右端点+1)-1,累积起来就是覆盖次数。