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\)
總結
樹上的父親-兒子
類限制可以思考繫結類貪心模型。
#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");
}
}