并查集应用(10)
给定一棵树,每次有两种操作:
• 加边
• 询问两个点是否存在⾄少两条不相交的路径
两个点之间是否存在至少两条不相交的路径就等价于是否在同一个边双里面。
考虑在树上(u, v)之间连边,会形成一个经过(u, v, lca_{u, v})的环,我们需要在加边之后把这个新形成的环上的点所在的边双都合并在一起。
那么这需要一个往上条的过程,暴力显然不行,可以用并查集维护边双,这样就能快速找到边双里深度最小的就能实现快速跳跃。
时间复杂度是(O(n alpha(n)))。
(2-SAT)经典问题(1.5)
给定(n)个(01)串,每个串中有(1)个星号。
问:是否存在一种把星号填写为(0/1)的方式,使得不存在一个串是另一个的前缀。
建出来(trie)树发现找到星号点所在的位置,就相当于选(0)或者(1)走下去,那么观察一下发现,如果选择的点存在祖先后代的关系就一定会出现前后缀,所以对于点存在祖先后代关系的点(u)和(v),选择了(u)就必须选择(ar{v}),选择了(v)就必须选择(ar{u}),这就是个朴素的(2-SAT)了。
(2-SAT)经典问题(1.6)
给定(n)个人,每个人可以在数轴上((a_i, b_i))两个位置中选一个站。
安排所有人的站位,最小化距离最近的两人之间的距离。
(n leq 1e5)
二分最后的答案,那么和某个点距离小于答案的点都不能同时选,所以连边跑(2-SAT),最多可能有(n^2)条边,考虑每个点连向一个区间的点,所以可以用线段树优化连边,这样复杂度就是(O(n log{n} log{n}))的了。
最短路经典问题(2.2)
给定(n)个点的有权无向图,每个点另外有一个点权。
定义一条路径的代价是 路径总长加经过的点的最大点权。
求每对点之间的最短路,(N leq 500)。
每个点按点权排序,每次(O(n ^ 2))用(Floyed)更新,用最短路加当前最大点权更新答案。
区间图
对于一般的图,有团数小于等于色数,最大独立集小于等于最小团覆盖,但是在弦图这样的完美图上,这两个都是相等的。
团数小于等于色数是因为一个团里的所有点颜色肯定不同。
最大独立集小于等于最小团覆盖是因为,最大独立集中的任意两个点都不能在同一个团里出现。
然后在区间图上这两个就都可以构造出来。
色数就是按右端点排序然后从右往左扫,对于每个点取一个和他相连的所有点的颜色不同的颜色。
最小团覆盖就是因为每个点和他相连的所有点构成一个团,所以每次对于一个点取出所有和它相连的点构成一个团即可。
那这样一些经典的区间贪心问题就可以通过区间图证明。
找最多的区间使得两两不想交,这就是求区间图的最大独立集。
将区间分成若干集合使得每个集合里区间不相交,这就是求区间图的色数。
找一个点使得经过它的区间个数最多,这就是求区间图的团数。
划分为最少的组,使得每组内区间两两有交,这就是求区间图的最小团覆盖。
单调队列优化多重背包
最朴素的(dp)式子是
令(p = left lfloor frac{j}{c_i} ight floor),(q = j mod c_i)。
代入上式,有
尝试将(p - k)用(t)替换掉,代入有
稍微做下小变形,有
所以只需要求出里面东西的最大值即可,注意到里面的东西只和(t)和(q)有关,枚举掉(q)之后有用(t)是连续的一段,所以可以用单调队列优化掉。
/*
_______ ________ _______
/ _____ / ______ / _____
/ / \_ _ __ _ / / _ __ _ / / \_
| | | | | | | | | | | | | | | | | | | |
| | | | | | | | | | __ | | | | | | | | | |
| | __ | | / / | | | | | | / / | | __
\_____/ / / / / / \_____ / / / / / \_____/ /
\_______/ \___/ \___/ \______/\__ \___/ \___/ \_______/
*/
#include <iostream>
#include <cstdio>
#include <cstring>
using namespace std;
const int N = 5000;
int w[N + 50], num[N + 50], c[N + 50], dp[N + 50], head, tail, n, m;
struct Node {
int zhi, pos;
} Q[N + 50];
void Read(int &x) {
x = 0; int p = 0; char st = getchar();
while (st < '0' || st > '9')
p = (st == '-'), st = getchar();
while (st >= '0' && st <= '9')
x = (x << 1) + (x << 3) + st - '0', st = getchar();
x = p ? -x : x;
return;
}
int main() {
int t;
Read(t);
while (t--) {
Read(m); Read(n);
for (int i = 1; i <= n; i++)
Read(c[i]), Read(w[i]), Read(num[i]);
memset(dp, 0, sizeof(dp));
for (int i = 1; i <= n; i++) {
int lim = min(num[i], m / c[i]);
for (int q = 0; q < c[i]; q++) {
head = tail = 1;
Q[tail] = (Node){0, 0};
for (int p = 0; p <= (m - q) / c[i]; p++) {
while (head <= tail && Q[head].pos < p - lim) head++;
while (head <= tail && Q[tail].zhi <= dp[p * c[i] + q] - p * w[i]) tail--;
Q[++tail] = (Node){dp[p * c[i] + q] - p * w[i], p};
dp[p * c[i] + q] = max(Q[head].zhi + p * w[i], dp[p * c[i] + q]);
}
}
}
printf("%d
", dp[m]);
}
return 0;
}
树形背包复杂度证明
就是这题的复杂度证明。
分成三部分来讨论。
临界值是对于每个点从根节点到它的路径上找到第一个子树大小小于等于(k)的节点。
那么对于每个点在这个子树里,都会和其它的每个点产生(O(size))的贡献,总体是(O(n imes size) leq O(n k))。
然后这个点往上合并到临界节点。
临界节点和别的子树合并完成后大小会大于(k),它跟别的任意东西合并的复杂度最多是(O(k))的。
然后每个点都只有一个临界节点,所以这部分复杂度贡献也是(O(n k))。
最后观察这些大小大于(k)的点,发现最多有(O(frac{n}{k}))个,每次合并复杂度(O(k ^ 2)),所以总体(O(n k))。
宝藏的最优时间复杂度做法
对于这题的复杂度分析。
(dp_{i, s})表示现在深度是(i),已经处理完(s)这个点集的最小代价。
每次先(O(2 ^ m))枚举集合(s),然后(O(3 ^ {n - m}))枚举它在全集中的补集,预处理出(minn_{s1, s2})表示(s2)中的点向(s1)中的点连边的最小值之和,转移是(O(1))的,复杂度(O(3 ^ n))。
然后要枚举哪个点是开始节点所以是(O(3 ^ n n))。
这样转移的道理是所有边权都是非负的,所以如果某个点连到了不是(s)中最后一层的点,那么它对答案的贡献会多算,不如别的情况中更优;即不合法情况总会有合法情况比它更优。
预处理这个东西可以做到(O(3 ^ n)),(O(2 ^ m))枚举(s1),(O(3 ^ {n - m}))枚举(s2),再预处理出某个点向(s)中连边的最小值,转移也是(O(1))的。
而再预处理这个东西就可以(O(2 ^ n n))或者(O(2 ^ n n ^ 2))做,综上,时间复杂度(O(3 ^ n n))。