1. 程式人生 > 其它 >JOI 2015 FInal 舞會

JOI 2015 FInal 舞會

JOI 2015 FInal 舞會

​ 題目連結:#2727. 「JOI 2015 Final」舞會 - 題目 - LibreOJ (loj.ac)


​ 很明顯,答案具有單調性,我們考慮二分答案。那麼問題就變成了怎麼寫出 check 函式。

​ 我們發現,根據題意,每三個點最後都會變成一個點,那我們不斷進行縮點,畫個圖看看?

​ 我們發現,這個數列變成了一棵樹,這棵樹的根就是最後留下來的點,我們可以利用這棵樹來檢驗。注意到,這棵樹中的每個非葉子節點都有且僅有三個兒子,且他的值是他三個兒子的中位數。對於中位數的題目我們又有一個很套路的做法,二分中位數,將大於等於中位數的數變成1,小於中位數的數變成0。

這剛好契合上我們之前二分答案的思想。那麼判斷這個答案可不可行就轉化成了在轉化數之後根節點的值能不能為1這個問題了。

​ 至於建樹,我們模擬過程就好了,程式碼像這樣:

void build()
{
	queue <int> q;
	for(int i=1;i<=n;++i)
		q.push(i);
	int tot=n;
	while(q.size()>1)
	{
		int oo=++tot;
		if(q.size()==3) root=oo;
		for(int i=1;i<=3;++i)
			add(oo,q.front()),q.pop();
		q.push(oo);
	}
}

​ 然後我們研究樹上節點的值是怎麼來的。注意到,每個葉子節點的值與他這個位置的貴族的熟練度有關,兒每個非葉子節點的值就是它三個兒子的中位數。也就是說當且僅當這個節點的三個兒子中至少有兩個1,這個節點的值才為1。而葉子節點的值是可以靠人去分配的,同時葉子結點的值的個數也是一定的,那麼樹形dp的做法就呼之欲出了。我們遍歷一遍每個貴族的熟練度,求出我們手中擁有1的個數,再通過樹形dp求出要讓根節點的值為1最小需要1值的個數,再講這個數與我們手上擁有1值的個數相比較就可以完成判斷。

​ 我們設 dp[i] 為使 \(i\) 這個點的值為1所需要再新增的1值的個數。我們又要讓dp值最小,那麼轉移式為:

\[dp_i=dp_{son1}+dp_{son2}+dp_{son3}-\max(dp_{son1},dp_{son2},dp_{son3}) \]

​ 因為我們只需要兩個1就可以了,付出代價最大的那個節點就讓他為0。然後我們看看初始值怎麼賦。如果這個點不是一個貴族一開始的固定的位置,那麼這個點的 \(dp\) 值就為1。如果是的話,再考慮貴族的值是不是1,如果是1,那麼這個點的 \(dp\) 值為0。否則為 \(inf\) (注:這裡的 \(inf\) 開小一點,否則容易溢位導致負數的出現)

​ 初始值我們可以這麼賦:

	memset(dp,0,sizeof dp);
	for(int i=1;i<=n;++i) dp[i]=1;
	for(int i=1;i<=m;++i)
	{
		if(d[i]>=x) dp[p[i]]=0;
		else dp[p[i]]=1e7;
	}

全部程式碼如下:

#include<bits/stdc++.h>
using namespace std;
const int MAXN = 1e5+5;
int n,m,d[MAXN],p[MAXN],v[MAXN],dp[MAXN<<2],root;
int head[MAXN<<2],all;
struct E
{
	int to,pre;
}e[MAXN<<2];
void add(int u,int v)
{
	e[++all]=E{v,head[u]};
	head[u]=all;
}
void build()
{
	queue <int> q;
	for(int i=1;i<=n;++i)
		q.push(i);
	int tot=n;
	while(q.size()>1)
	{
		int oo=++tot;
		if(q.size()==3) root=oo;
		for(int i=1;i<=3;++i)
			add(oo,q.front()),q.pop();
		q.push(oo);
	}
}
void dfs(int k)
{
	if(k<=n) return ;
	int Max=0;
	for(int i=head[k];i;i=e[i].pre)
	{
		int to=e[i].to;
		dfs(to);
		dp[k]+=dp[to];
		Max=max(Max,dp[to]);
	}
	dp[k]-=Max;
}
bool check(int x)
{
	memset(dp,0,sizeof dp);
	for(int i=1;i<=n;++i) dp[i]=1;
	for(int i=1;i<=m;++i)
	{
		if(d[i]>=x) dp[p[i]]=0;
		else dp[p[i]]=1e7;
	}
	int cnt=0;
	for(int i=m+1;i<=n;++i) if(d[i]>=x) ++cnt;
	dfs(root);
	return dp[root]<=cnt;
}
int main()
{
	scanf("%d %d",&n,&m);
	for(int i=1;i<=m;++i)
		scanf("%d %d",&d[i],&p[i]);
	for(int i=m+1;i<=n;++i)
		scanf("%d",&d[i]);
	for(int i=1;i<=n;++i)
		v[i]=d[i];
	sort(v+1,v+1+n);
	int Siz=unique(v+1,v+1+n)-v-1;
	build();
	int l=1,r=Siz,ans=1;
	while(l<=r)
	{
		int mid=l+r>>1;
		if(check(v[mid]))
		{
			ans=mid;
			l=mid+1;
		}
		else r=mid-1;
	}
	printf("%d\n",v[ans]);
	return 0;
}

​ 像這種有點像模擬,需要不斷進行操作的題目。對於這種陌生的模型,我們可以將模型轉化為我們熟悉的某種圖進行解題。一般來說,一種操作就相當於在圖上進行一次移動。這種轉化模型的圖還是比較考驗想法的。這就是我被初見殺的理由0.0 還是得靠做題積累經驗。

路漫漫其修遠兮,吾將上下而求索。