NOIP模拟赛20191026 T3 Repulsed【树上控制型贪心】

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接: https://blog.csdn.net/C20181220_xiang_m_y/article/details/102758200

题目描述:

n个点的树,每个节点可以放置任意个灭火器,每个灭火器可以浇灭距离不超过k条边的火,且最多只能浇灭s个点的火。现在n个点全都有火,问把火全部浇灭的最少灭火器数量。

n<=100000,k<=20,s<=109

题目分析:

这个题,如果没有s的限制,就是个提高-的贪心题。可以dfs从下往上传最浅的灭火器和最深的没有被扑灭的火,然后匹配或者新加灭火器,复杂度O(n)。或者按照深度从大到小枚举点,检查往上k个点是否有灭火器,如果没有就放一个然后更新再往上的k个点(查询时检验深度之和,具体方法见下),复杂度O(nk)。

现在有了s的限制怎么做,考虑同样的方法。

先看看dfs是否可行,我们需要知道子树中还未被扑灭的火的深度,除此之外还要知道对应的数量,以及灭火器的深度及还可以用的数量。
先考虑从下往上传时用堆维护,首先,如果有离当前点距离为k的点时,显然需要在当前点放置灭火器。
然后,很自然地会想到让子树内的火和灭火器配对,但是在有个数限制的情况下子树内配对并不是最优的,会出现这样的情况:
在这里插入图片描述
所以O(nlogn)的方法行不通,但是当灭火器和火的距离之和为k以及k-1时,一定会在子树内匹配:
在这里插入图片描述
而当灭火器和火的距离<=k-2时,我们可以把问题留到当前点的父亲去解决,并不会有什么影响。
所以我们可以记录 f [ i ] [ j ] f[i][j] g [ i ] [ j ] g[i][j] 分别表示 i i 子树内距离 i i j j 的灭火器和火的数量,然后将距离和为k的配对,再将和为k-1的配对,然后往上传即可,复杂度O(nk)

Code:

#include<bits/stdc++.h>
#define maxn 100005
#define maxk 25
using namespace std;
int n,s,k,dep[maxn],f[maxn][maxk],g[maxn][maxk],ans;//f:put out g:fire need
int fir[maxn],nxt[maxn<<1],to[maxn<<1],tot;
inline void line(int x,int y){nxt[++tot]=fir[x],fir[x]=tot,to[tot]=y;}
void dfs(int u,int ff){
	for(int i=fir[u],v;i;i=nxt[i]) if((v=to[i])!=ff){
		dfs(v,u);
		for(int j=0;j<k;j++) f[u][j+1]=min(f[u][j+1]+f[v][j],n),g[u][j+1]+=g[v][j];
	}
	g[u][0]=1; int x;
	if(g[u][k]) ans+=(x=(g[u][k]+s-1)/s),f[u][0]=min(s*x,n);
	for(int i=0;i<=k;i++) x=min(f[u][i],g[u][k-i]),f[u][i]-=x,g[u][k-i]-=x;
	for(int i=0;i<k;i++) x=min(f[u][i],g[u][k-i-1]),f[u][i]-=x,g[u][k-i-1]-=x;
}
int main()
{
	scanf("%d%d%d",&n,&s,&k);
	for(int i=1,x,y;i<n;i++) scanf("%d%d",&x,&y),line(x,y),line(y,x);
	dfs(1,0);
	for(int i=0,x;i<=k;i++)
		for(int j=k-i;j>=0&&f[1][i];j--)
			x=min(f[1][i],g[1][j]),f[1][i]-=x,g[1][j]-=x;
	int sum=0; for(int i=0;i<=k;i++) sum+=g[1][i];
	printf("%d\n",ans+(sum+s-1)/s);
}

再来看看向上检测k的方法是否可行。

按深度由大到小枚举点,向上查找是否有灭火器,如果没有,假设当前火向上k个点为 u u ,那么在 u u 点新加一个灭火器,同时用u点的灭火器编号插入u往上1~k个点的set中,set按编号对应深度排序,查找时就在set中查找能够配对的最深的灭火器配对(不删除),一个全局数组记录每个编号灭火器的个数,当个数减为0时就将该编号从u往上1~k个点的set中删去。

看似十分可行,需要检验正确性的就是"找能够配对的最深的灭火器配对"这一点:
在这里插入图片描述
(%Freopen的Code):

#include<bits/stdc++.h>
#define maxn 100005
#define pii pair<int,int>
#define mp make_pair
#define pb push_back
using namespace std;

int n,s,k;
int dep[maxn],fa[maxn],id[maxn];
int info[maxn],Prev[maxn<<1],to[maxn<<1],cnt_e;
void Node(int u,int v){ Prev[++cnt_e]=info[u],info[u]=cnt_e,to[cnt_e]=v; }
bool cmp(const int &u,const int &v){ return dep[u]>dep[v]; }
void dfs(int u,int ff){
	id[u] = u;
	dep[u] = dep[fa[u] = ff] + 1;
	for(int i=info[u],v;i;i=Prev[i])
		if((v=to[i])!=ff)
			dfs(v,u);
}
int hd[maxn],cnt;
vector<pair<set<pii>::iterator,int> >cp[maxn];
set<pii>st[maxn];


int main(){
	freopen("repulsed.in","r",stdin);
	freopen("repulsed.out","w",stdout);
	scanf("%d%d%d",&n,&s,&k);
	for(int i=1,u,v;i<n;i++) scanf("%d%d",&u,&v),Node(u,v),Node(v,u);
	dfs(1,0);
	sort(id+1,id+1+n,cmp);
	for(int i=1;i<=n;i++){
		int u = id[i];
		pii ret = mp(0,-1);
		int pr=0;
		for(int j=u,stp=0;stp<=k && j;stp++,j=fa[pr=j]){
			set<pii>::iterator it = st[j].upper_bound(mp(k-stp+dep[j],0x3f3f3f3f));
			if(it == st[j].begin()) continue;
			it--;
			ret = max(ret , *it);
		}
		if(ret.second == -1){
			hd[cnt] = s - 1;
			if(hd[cnt])
				for(int j=pr,stp=0;stp<=k && j;stp++,j=fa[j])
					cp[cnt].pb(mp(st[j].insert(mp(dep[pr],cnt)).first,j));
			cnt++;
		}
		else{
			int v;
			if(!--hd[v=ret.second]){
				for(int j=0,sz=cp[v].size();j<sz;j++)
					st[cp[v][j].second].erase(cp[v][j].first);
			}
		}
	}
	printf("%d\n",cnt);
}

猜你喜欢

转载自blog.csdn.net/C20181220_xiang_m_y/article/details/102758200