1. 程式人生 > 其它 >HNOI 2018 題目泛做

HNOI 2018 題目泛做

前言

受到 \(\tt werner\_yin\) 鴿鴿的啟發,我要開始刷 \(\tt HNOI\) 了。

還是每天更至少三題的部落格,\(\tt zxy\) 絕不斷更。

2018 排列

題目描述

點此看題

解法

題目描述特別混亂,觀察樣例解釋可以把問題轉化成:連一條 \(a_i\rightarrow i\) 的有向邊,要求如果 \((i,j)\) 有邊那麼需要滿足 \(p_i<p_j\),試確定 \(p\) 使得 \(\prod_{i=1}^n p_i\cdot w_i\) 最大。

首先如果構成環那麼答案一定是 \(-1\),否則圖的結構一定是以 \(0\) 為根的一棵有根樹。我們可以依次從 \(1\rightarrow n\)

在樹上選取對應的點,要求選某一個點之前必須選它的父親

到這裡其實我們把問題轉化成了一個經典模型:牛半仙的魔塔,也就是我們考慮無限制時會選取哪個點最優,但是實際上我們需要先選取它的父親才能選他,這說明選取它的父親之後一定會選他,這構成了一個很強的限制關係,所以我們在修正答案之後把它和父親繫結即可。

那麼我們如何判斷到底哪個"點"(實際上是一個繫結好順序的序列)才是最優的呢?設兩個序列 \(A,B\) 的長度分別是 \(m_1,m_2\),那麼考慮 \(A\) 放在前面的條件是:

\[W_{ab}-W_{ba}=m_1w_b-m_2w_a>0\Leftrightarrow\frac{w_a}{m_1}<\frac{w_b}{m_2} \]

也就是平均值小的點需要先行選取,繫結的時候直接更改 \(w\)

之和與序列長度,再把修改過後的"點"塞進優先佇列即可,新增的貢獻是 \(m_{fa}\cdot w_u\),時間複雜度 \(O(n\log n)\)

總結

樹上的父親-兒子類限制可以思考繫結類貪心模型。

#include <cstdio>
#include <vector>
#include <iostream>
#include <queue>
using namespace std;
#define int long long
const int M = 500005;
int read()
{
	int x=0,f=1;char c;
	while((c=getchar())<'0' || c>'9') {if(c=='-') f=-1;}
	while(c>='0' && c<='9') {x=(x<<3)+(x<<1)+(c^48);c=getchar();}
	return x*f;
}
int n,cnt,ans,a[M],siz[M],vis[M],fa[M],par[M];
vector<int> g[M];
struct node
{
	int rt,x,y;
	bool operator < (const node &r) const
	{
		return x*r.y>r.x*y;
	}
};priority_queue<node> q;
void dfs(int u,int fa)
{
	vis[u]=1;cnt++;par[u]=fa;
	for(int v:g[u]) if(!vis[v]) dfs(v,u);
}
int find(int x)
{
	if(x!=fa[x]) fa[x]=find(fa[x]);
	return fa[x];
}
signed main()
{
	n=read();
	for(int i=1;i<=n;i++)
	{
		int j=read();
		g[j].push_back(i);
	}
	dfs(0,0);
	if(cnt<=n) {puts("-1");return 0;}
	for(int i=0;i<=n;i++) siz[i]=1,fa[i]=i;
	for(int i=1;i<=n;i++)
		a[i]=read(),q.push(node{i,a[i],1});
	while(!q.empty())
	{
		node t=q.top();int u=t.rt;q.pop();
		if(siz[u]!=t.y) continue;
		int p=fa[u]=find(par[u]);
		ans+=a[u]*siz[p];a[p]+=a[u];siz[p]+=siz[u];
		if(p) q.push(node{p,a[p],siz[p]});c++
	}
	printf("%lld\n",ans);
}

2018 遊戲

題目描述

點此看題

解法

不難發現每個點能到達的範圍是 \([l_i,r_i]\) 的形式,針對這類問題我們需要有繼承的思想,也就是如果你走到點 \(k\),那麼你走到 \([l_k,r_k]\) 這個範圍就是充分的,所以你可以繼承點 \(k\) 的資訊來加速計算。

首先考慮 \(y\leq x\) 的簡單情況,也就是所有鑰匙一定在房間的左邊,那麼很顯然我們只能向右通過上鎖的門。設 \(a_x\) 表示門 \((x,x+1)\) 的鑰匙放的位置,那麼門能通過的條件是 \(a_x\geq l_i\)(注意此時 \(l_i\) 是一開始就知道的),我們從右到左掃描,用一個單調棧來維護那個作為瓶頸的門,根據繼承的思想當一個門不再成為瓶頸的時候是可以直接彈出的,最後我們取棧頂繼承。

回到本題,複雜的地方是 \(l_i\) 是隨著 \(r_i\) 變化的,那麼我們只需要在 \(r_i\) 變化時快速計算出 \(l_i\) 即可。考慮左邊鑰匙在更左邊的門一定是過不去的,我們可以預處理出左端點的大致範圍 \(l_i\geq ls_i\),那麼剩下的門只用考慮鑰匙在右邊的情況。此時能走過的條件是:\(a_x\leq r_i\),我們用線段樹處理出 \([ls_i,prel_i)\) 之間的第一個大於 \(r_i\) 的位置,那麼時間複雜度 \(O(n\log n)\)

#include <cstdio>
#include <iostream>
using namespace std;
const int M = 1000005;
int read()
{
	int x=0,f=1;char c;
	while((c=getchar())<'0' || c>'9') {if(c=='-') f=-1;}
	while(c>='0' && c<='9') {x=(x<<3)+(x<<1)+(c^48);c=getchar();}
	return x*f;
}
int n,m,q,s[M],a[M],l[M],r[M],ls[M],tr[M<<2];
void build(int i,int l,int r)
{
	if(l==r) {tr[i]=a[l];return ;}
	int mid=(l+r)>>1;
	build(i<<1,l,mid);
	build(i<<1|1,mid+1,r);
	tr[i]=max(tr[i<<1],tr[i<<1|1]);
}
int find(int i,int l,int r,int x)
{
	if(tr[i]<=x) return 0;
	if(l==r) return l;
	int mid=(l+r)>>1;
	return tr[i<<1|1]>x?find(i<<1|1,mid+1,r,x):
	find(i<<1,l,mid,x);
}
int ask(int i,int l,int r,int L,int R,int x)
{
	if(L>r || l>R) return 0;
	if(L<=l && r<=R) return find(i,l,r,x);
	int mid=(l+r)>>1,p=ask(i<<1|1,mid+1,r,L,R,x);
	if(p) return p;
	p=ask(i<<1,l,mid,L,R,x);
	return p;
}
int get(int l,int r,int x)
{
	if(l>r) return l;
	int p=ask(1,1,n,l,r,x);
	return p?p+1:l;
}
void init()
{
	build(1,1,n);a[n]=n+1;a[0]=-1;
	for(int i=1;i<=n;i++)
		ls[i]=a[i-1]&&a[i-1]<=i-1?i:ls[i-1];
	for(int i=n,t=0;i>=1;i--)
	{
		l[i]=r[i]=i;
		l[i]=get(ls[i],l[i]-1,r[i]);
		s[++t]=i;
		while(t && (!a[s[t]] || l[i]<=a[s[t]] && a[s[t]]<=r[i]))
		{
			r[i]=s[--t];
			l[i]=get(ls[i],l[i]-1,r[i]);
		}
	}
}
signed main()
{
	n=read();m=read();q=read();
	for(int i=1,x,y;i<=m;i++)
		x=read(),y=read(),a[x]=y;
	init();
	while(q--)
	{
		int x=read(),y=read();
		puts(l[x]<=y && y<=r[x]?"YES":"NO");
	}
}