[NWRRC2015] Insider's Information
一、题目
二、解法
我们从两侧往中间插入,那么三元组 \((a,b,c)\) 合法的必要条件是:\(a\) 或 \(c\) 先于 \(b\) 插入。
发现我们可以很轻易地让所有三元组都满足必要条件,方法是连边 \(a\rightarrow b\) 和 \(c\rightarrow b\),但是只让 \(b\) 的入度增加 \(1\) ,然后跑拓扑排序,只要按照拓扑序插入即可保证 \(a\) 或 \(c\) 先于 \(b\) 插入。由于题目条件 保证存在一个排列满足所有限制
,可知任何时刻都会有入度为 \(0\) 的点,那么一定也存在这样的拓扑序。
下一步是依照拓扑序,决定每个点是放最左边还是放在最右边。考虑三元组 \((a,b,c)\) 在拓扑序上可能的顺序有:(a,b,c),(a,c,b),(c,b,a),(c,a,b)
,由于 \(a,c\) 是对称的,我们不妨只考虑 \(a\) 已经被放在最左边的情况:
- 若顺序是
(a,b,c)
,那么产生贡献的充要条件是 \(b\) 被放在最左边。 - 若顺序是
(a,c,b)
,那么产生贡献的充要条件是 \(c\) 被放在最右边。
这说明:我们只需要在将要加入第二个元素的时候考虑这个限制。实现时把拓扑排序和决定位置同时进行,假设现在要插入点 \(u\),我们考虑 \(u\) 作为 \(b\) 的限制(对应的 \(a\) 已经被插入),和 \(u\) 作为 \(c\) 的限制(对应的 \(a\) 已经被插入),可以统计 \(u\) 放在最左边的贡献和 \(u\) 放在最右边的贡献,去较大的那边放置即可。
时间复杂度 \(O(n)\),由于每次都是少数服从多数,被满足的限制至少有 \(\lceil\frac{m}{2}\rceil\) 个。
三、总结
我觉得本题的最大亮点就是把限制放在单点上,为达成这一目的,我们可能需要先拆解限制(比如本题一开始的必要条件),然后在若干先决条件的基础上可以在单点上进行决策。类似精妙拆限制的题还有:JOISC 2015 Day2 Keys
#include <cstdio>
#include <vector>
#include <iostream>
#include <queue>
using namespace std;
const int M = 100005;
#define pb push_back
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,a[M],b[M],c[M],d[M],p[M],ans[M];
vector<int> g[M],s[M];queue<int> q;
signed main()
{
n=read();m=read();
for(int i=1;i<=m;i++)
{
a[i]=read();b[i]=read();c[i]=read();
d[b[i]]++;s[b[i]].pb(i);
g[a[i]].pb(i);g[c[i]].pb(i);
}
for(int i=1;i<=n;i++)
if(!d[i]) q.push(i);
int l=1,r=n;
while(!q.empty())
{
int u=q.front(),w[2]={};q.pop();
for(int v:g[u])
{
if(p[a[v]] || p[c[v]])
w[p[u^a[v]^c[v]]<l]++;
else if(!--d[b[v]]) q.push(b[v]);
}
for(int v:s[u])
if(!(p[a[v]] && p[c[v]]))
w[(p[a[v]]^p[c[v]])>r]++;
p[u]=w[0]<w[1]?r--:l++;
}
for(int i=1;i<=n;i++) ans[p[i]]=i;
for(int i=1;i<=n;i++) printf("%d ",ans[i]);
puts("");
}