【luogu CF1119F】【luogu P7600】Niyaz and Small Degrees / 封闭道路

Niyaz and Small Degrees / 封闭道路

题目链接:luogu CF1119F / luogu P7600

题目大意

给一棵树,边有边权,是砍掉的费用。
对于每个 x,询问最少要花费费用砍边,使得每个点都不与超过 x 个点相连。

思路

首先看到题目容易想到要 DP。
那我们就想想 n 2 l o g n n^2logn n2logn 的 DP,我们枚举 x x x,然后搞 O ( n l o g n ) O(nlogn) O(nlogn) DP。

那就是设 f i , 0 / 1 f_{i,0/1} fi,0/1​ 为 i i i 与父亲连边删或不删时以 i i i 为根的子树的点都满足度数不大于 x x x 的最小费用。
在度数没有限制的时候,那对于 u u u 的儿子 v v v,之间边权为 w w w,有 f u , 0 = f u , 1 = min ⁡ { f v , 0 , f v , 1 + w } f_{u,0}=f_{u,1}=\min\{f_{v,0},f_{v,1}+w\} fu,0​=fu,1​=min{fv,0​,fv,1​+w}

那如果有了度数限制,那我们就要想,你选左边就是割,右边就是不割。
那对于 f u , 0 f_{u,0} fu,0​,它就一定要选 d u u − x du_u-x duu​−x 个左边的。( d u u du_u duu​ 为 u u u 的度数)
那对于 f u , 1 f_{u,1} fu,1​,它就一定要选 d u u − x − 1 du_u-x-1 duu​−x−1 个左边的。
那容易想到,如果本来就是左边优,那我们肯定选左边。那我们可以提前选,然后记得要选的个数要减。
那如果是右边优,那我们就看如果硬要选左边会亏多少( f v , 1 + w − f v , 0 f_{v,1}+w-f_{v,0} fv,1​+w−fv,0​),那我们肯定优先硬改亏的最少的,那我们可以用堆来找到前 d u u − x du_u-x duu​−x 个的总费用。
那就可以了。

那我们考虑怎么优化。
不难想到,如果 x ≥ d u u x\geq du_u x≥duu​,那 u u u 这个点割不割都无所谓,那我们可以把这个点删掉。
但不能把别的点入度修改,而且别的点就相当于有一个费用是 w w w( w w w 是它与这个点的边的边权)放进了堆里面。而且这个是一只存在的,不像我们上面算法的堆,那些要清空,而这些要留下。

那我们就要弄一个可删堆,可以用两个普通的堆来维护。
(大概就是你要删你就不直接删而是放进删除堆,每次你要取之前如果你的普通堆和删除堆堆顶相同就把它弹出不要,即我们不立刻删掉,而是阻碍到再删)
(具体是看看代码)

那我们接着继续想,那每次枚举 x x x,DP 的规模就不是 n l o g n nlogn nlogn,而是 m l o g m mlogm mlogm( m m m 时度数超过 x x x 的点数)
那这个所有 m m m 加起来的规模其实是 O ( n ) O(n) O(n) 级别的,因为你想,入度和是 2 n 2n 2n,那一个点入度是 x x x 就会贡献 x x x 次,那总共就最多贡献 2 n 2n 2n 次,所以 m m m 就是 O ( n ) O(n) O(n) 级别。
复杂度也就变成了 O ( n l o g n ) O(nlogn) O(nlogn)

具体的实现可以看看代码。

代码

由于两道题是一模一样的,而且 APIO 由于那个交互代码会有点乱,这里就只在 CF 的那个上面写注释。

然后记得两个的点编号一个是 1 ∼ n 1\sim n 1∼n,一个是 0 ∼ n − 1 0\sim n- 1 0∼n−1。

Niyaz and Small Degrees

#include<queue>
#include<cstdio>
#include<vector>
#include<algorithm>
#define ll long long

using namespace std;

int n, x, y, z, du[250001], xx[250001];
int nd, kil, in[250001];
vector <pair<int, int> > e[250001];
ll sum, f[250001][2], re;
vector <ll> ans, tmp, del;

struct cd_heap {//可删堆
	priority_queue <int> q1, q2;
	ll sum;
	void insert(int x) {q1.push(x); sum += x;}
	void delete_(int x) {q2.push(x); sum -= x;}
	void ck() {while (!q1.empty() && !q2.empty() && q1.top() == q2.top()) q1.pop(), q2.pop();}
	int top() {ck(); return q1.top();}
	void pop() {ck(); sum -= q1.top(); q1.pop();}
	int size() {return q1.size() - q2.size();}
}h[250001];

void add(int x, int y, int z) {
	e[x].push_back(make_pair(y, z));
	du[x]++;
}

bool cmp0(pair <int, int> x, pair <int, int> y) {
	return du[x.first] > du[y.first];
	//优化,枚举相连的点时按相连点入度从大到小枚举
	//如果枚举到已被删去,那后面的点都被删了,就可以直接退出了
}

bool cmp(int x, int y) {
	return du[x] < du[y];
}

void kill(int x) {//删点
	for (int i = 0; i < e[x].size(); i++){
		int y = e[x][i].first, z = e[x][i].second;
		if (du[y] <= nd) break;
		h[y].insert(z);//相连的边要留着,相当于单独费用是 z 的(因为不砍不会有花费,z-0=z)
	}
}

void dfs(int now, int father) {
	in[now] = nd;
	int num = du[now] - nd;//看要砍多少边
	while (h[now].size() > num)//维护堆只有这么多个边(下同)
		h[now].pop();
	
	for (int i = 0; i < e[now].size(); i++) {//dfs 递归 DP
		int to = e[now][i].first;
		if (du[to] <= nd) break;
		if (to == father) continue;
		dfs(to, now);
	}
	
	tmp.clear(); del.clear();//记得清空
	for (int i = 0; i < e[now].size(); i++) {
		int to = e[now][i].first, dis = e[now][i].second;
		if (du[to] <= nd) break;
		if (to == father) continue;
		ll x = f[to][1] + dis - f[to][0];
		if (x <= 0) {//明显是不割优
			re += f[to][1] + dis;
			num--;//这个直接处理乘割了,那要割的数量就少了
		}
		else {
			re += f[to][0];
			del.push_back(x);//把这里加进堆的记录下来,因为只有这一次可以用,那我们搞完要删掉它们
			h[now].insert(x);
		}
	}
	
	//tmp 是存丢出去的,由于也是只是这一次用,那我们到时还要放回去,丢出去只是为了统计费用
	while (h[now].size() && h[now].size() > num)
		tmp.push_back(h[now].top()), h[now].pop();
	f[now][0] = h[now].sum;//不删父亲的
	while (h[now].size() && h[now].size() > num - 1)//删父亲就代表要多删一条
		tmp.push_back(h[now].top()), h[now].pop();
	f[now][1] = h[now].sum;//删父亲的
	
	for (int i = 0; i < tmp.size(); i++)//记得把要还原的还原,把要删的删了
		h[now].insert(tmp[i]);
	for (int i = 0; i < del.size(); i++)
		h[now].delete_(del[i]);
}

int main() {
	scanf("%d", &n);
	for (int i = 1; i < n; i++) {
		scanf("%d %d %d", &x, &y, &z);
		add(x, y, z);
		add(y, x, z);
		sum += z;
	}
	
	ans.push_back(sum);
	for (int i = 1; i <= n; i++) {
		sort(e[i].begin(), e[i].end(), cmp0);
		xx[i] = i;
	}
	
	sort(xx + 1, xx + n + 1, cmp);
	
	kil = 1;
	while (++nd < n) {
		while (kil <= n && nd == du[xx[kil]]) kill(xx[kil]), kil++;//有新的可以直接删掉的点
		if (kil > n) {//所有点都被删掉了,后面答案都是 0
			ans.push_back(0);
			continue;
		}
		
		re = 0;
		for (int i = kil; i <= n; i++) {//DP,记得要枚举每个树
			if (in[xx[i]] == nd) continue;
			dfs(xx[i], 0);	
			re += f[xx[i]][0];
		}
		
		ans.push_back(re);
	}
	
	for (int i = 0; i < ans.size(); i++)
		printf("%lld ", ans[i]);
	
	return 0;
}

封闭道路

#include<queue>
#include<cstdio>
#include<vector>
#include<algorithm>
#define ll long long

using namespace std;

int n, x, y, z, du[250001], xx[250001];
int nd, kil, in[250001];
vector <pair<int, int> > e[250001];
ll sum, f[250001][2], re;
vector <ll> ans, tmp, del;

struct cd_heap {
	priority_queue <int> q1, q2;
	ll sum;
	void insert(int x) {q1.push(x); sum += x;}
	void delete_(int x) {q2.push(x); sum -= x;}
	void ck() {while (!q1.empty() && !q2.empty() && q1.top() == q2.top()) q1.pop(), q2.pop();}
	int top() {ck(); return q1.top();}
	void pop() {ck(); sum -= q1.top(); q1.pop();}
	int size() {return q1.size() - q2.size();}
}h[250001];

void add(int x, int y, int z) {
	e[x].push_back(make_pair(y, z));
	du[x]++;
}

bool cmp0(pair <int, int> x, pair <int, int> y) {
	return du[x.first] > du[y.first];
}

bool cmp(int x, int y) {
	return du[x] < du[y];
}

void kill(int x) {
	for (int i = 0; i < e[x].size(); i++){
		int y = e[x][i].first, z = e[x][i].second;
		if (du[y] <= nd) break;
		h[y].insert(z);
	}
}

void dfs(int now, int father) {
	in[now] = nd;
	int num = du[now] - nd;
	while (h[now].size() > num)
		h[now].pop();
	
	for (int i = 0; i < e[now].size(); i++) {
		int to = e[now][i].first;
		if (du[to] <= nd) break;
		if (to == father) continue;
		dfs(to, now);
	}
	
	tmp.clear(); del.clear();
	for (int i = 0; i < e[now].size(); i++) {
		int to = e[now][i].first, dis = e[now][i].second;
		if (du[to] <= nd) break;
		if (to == father) continue;
		ll x = f[to][1] + dis - f[to][0];
		if (x <= 0) {
			re += f[to][1] + dis;
			num--;
		}
		else {
			re += f[to][0];
			del.push_back(x);
			h[now].insert(x);
		}
	}
	
	while (h[now].size() && h[now].size() > num)
		tmp.push_back(h[now].top()), h[now].pop();
	f[now][0] = h[now].sum;
	while (h[now].size() && h[now].size() > num - 1)
		tmp.push_back(h[now].top()), h[now].pop();
	f[now][1] = h[now].sum;
	
	for (int i = 0; i < tmp.size(); i++)
		h[now].insert(tmp[i]);
	for (int i = 0; i < del.size(); i++)
		h[now].delete_(del[i]);
}

std::vector<long long> minimum_closure_costs(int N, std::vector<int> U,
                                             std::vector<int> V,
                                             std::vector<int> W) {
    n = N;
	for (int i = 1; i < n; i++) {
		x = U[i - 1] + 1;
		y = V[i - 1] + 1;
		z = W[i - 1];
		add(x, y, z);
		add(y, x, z);
		sum += z;
	}
	
	ans.push_back(sum);
	for (int i = 1; i <= n; i++) {
		sort(e[i].begin(), e[i].end(), cmp0);
		xx[i] = i;
	}
	
	sort(xx + 1, xx + n + 1, cmp);
	
	kil = 1;
	while (++nd < n) {
		while (kil <= n && nd == du[xx[kil]]) kill(xx[kil]), kil++;
		if (kil > n) {
			ans.push_back(0);
			continue;
		}
		
		re = 0;
		for (int i = kil; i <= n; i++) {
			if (in[xx[i]] == nd) continue;
			dfs(xx[i], 0);	
			re += f[xx[i]][0];
		}
		
		ans.push_back(re);
	}
	
	return ans;
}
上一篇:luogu P4173 残缺的字符串


下一篇:做题记录 Luogu P3808