Atcoder Grand Contest 001&002


001F Wide Swap

题目描述

点此看题

解法

话说我在考试时候乱打个做法得了很多分,但是这题还是要保证每一步严谨最后才能搞懂啊。

首先有一个明显的 \(\tt observation\)(我这个辣鸡都观察出来了哦!):我们求出逆排列 \(q_{p_i}=i\),那么排列 \(q\) 上的交换规则就是,对于相邻两个位置 \(i,i+1(1\leq i,如果 \(|q_i-q_{i+1}|\geq k\),那么则可以交换 \(i\)\(i+1\)最小化 \(p\) 的字典序转化成了让 \(q\) 较小的值所处的位置尽量小

暴力在 \(q\) 上做交换是难以维护的,这时候我们要思考排列 \(q\) 内隐含的偏序关系,可以得到一个关键性质:若 \(i,则最后 \(p'_{q_i}(也就是满足条件的两个值原来位置的偏序关系保持不变),这是因为这两个值永远不会越过彼此来交换。

可以根据这个拓扑关系来建立拓扑图,对于上述点我们将 \(q_j\) 连向 \(q_i\),边 \(u\rightarrow v\) 的含义是值 \(u\) 的位置要比值 \(v\) 的位置大。那么我们是想较小的值尽可能安排小的位置,但是由于拓扑关系的限制我们要把它能到达的值先安排了再说。

所以我们选择直接做拓扑排序,把位置 \(n\rightarrow 1\) 依次安排,我们首先把位置 \(n\) 给入度为 \(0\) 的最大的值,然后删除它之后继续安排位置 \(n-1......\)这样做可以把尽可能小的位置留给小的值。

那么问题变成了如何建立拓扑图,我们把 \(i\)\(1\rightarrow n\) 顺序扫描(保证了 \(i 的偏序关系),对于每个值 \(q_i\) 我们只需要找初始位置在 \((i-x,i)\)\((i,i+x)\) 的最大值连边即可,这样保证了拓扑关系又简化了拓扑图(可以理解成按值一层一层连边),用线段数维护一下最大值即可,时间复杂度 \(O(n\log n)\)

//You can check out any time you like
#include 
#include 
#include 
using namespace std;
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,k,p[M],q[M],d[M],mx[M<<2],ch[M][2];
void ins(int i,int l,int r,int id,int c)
{
	if(l==r) {mx[i]=c;return ;}
	int mid=(l+r)>>1;
	if(id<=mid) ins(i<<1,l,mid,id,c);
	else ins(i<<1|1,mid+1,r,id,c);
	mx[i]=max(mx[i<<1],mx[i<<1|1]); 
}
int ask(int i,int l,int r,int L,int R)
{
	if(L>r || l>R) return 0;
	if(L<=l && r<=R) return mx[i];
	int mid=(l+r)>>1;
	return max(ask(i<<1,l,mid,L,R),
	ask(i<<1|1,mid+1,r,L,R));
}
signed main()
{
	n=read();k=read();
	for(int i=1;i<=n;i++) q[read()]=i;
	for(int i=1;i<=n;i++)
	{
		int x=q[i];
		ch[x][0]=q[ask(1,1,n,x-k+1,x)];
		ch[x][1]=q[ask(1,1,n,x,x+k-1)];
		d[ch[x][0]]++;d[ch[x][1]]++;
		ins(1,1,n,x,i);
	}
	d[0]=-1;priority_queue q;
	for(int i=1;i<=n;i++) if(!d[i]) q.push(i);
	int nw=n;
	while(!q.empty())
	{
		int u=q.top();q.pop();p[u]=nw--;
		for(int i=0;i<2;i++)
			if(!(--d[ch[u][i]]))
				q.push(ch[u][i]);
	}
	for(int i=1;i<=n;i++)
		printf("%d\n",p[i]);
}

002F Leftmost Ball

题目描述

点此看题

解法

这道题想到结论但是没做出来确实是我 \(\tt naive\),太不灵活了呃呃呃。

我们只能去考察哪些数列是合法的,而不能按照题目给的方法去构造数列。我们列出尽可能多的必要条件:\(0\) 的数量和颜色的数量必须要对的上、\(\forall\) 前缀,\(0\) 个数不小于颜色个数

我们很容易证明上面的条件是充分的,我们可以从左往右顺序扫描,然后把白球填成第一个还没有匹配的颜色,白球一定是够用的,这样我们可以反向构造出原序列,充分性得证。

用这个充要条件来 \(dp\) 吧,考虑处理排列的两种方法:插入法和填空法。由于插入法我想了一年没有想出来,所以这里我们采用填空法。并且填空法是很好解决前面的限制的,我们保证填空的每个时刻都满足白球数不小于颜色数就是任意前缀都满足条件。

\(dp[i][j]\) 表示现在已经填入了 \(i\) 个白球和 \(j\) 种颜色,要满足 \(i\geq j\) 有效,那么转移考虑在第一个空位填入白球或者颜色,这样最契合前缀合法的限制,而且不会算重:

\[dp[i][j]=dp[i-1][j]+(n-j+1)\cdot {(n-j+1)(k-1)+n-i-1\choose k-2}\cdot dp[i][j-1] \]

最后就是 \(k=1\) 要特判,因为组合数都变成负数了显然有问题。

总结

对限制的感受能力还是太弱的,转移一定是根据限制来的,要去考虑转移的特殊性啊!

//But you can never leave
#include 
const int M = 2005;
const int N = M*M;
const int MOD = 1e9+7;
#define int long long
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,k,inv[N],fac[N],dp[M][M];
void init(int n)
{
	inv[0]=inv[1]=fac[0]=1;
	for(int i=2;i<=n;i++) inv[i]=inv[MOD%i]*(MOD-MOD/i)%MOD;
	for(int i=1;i<=n;i++) inv[i]=inv[i-1]*inv[i]%MOD;
	for(int i=1;i<=n;i++) fac[i]=fac[i-1]*i%MOD;
}
int C(int n,int m)
{
	if(m<0 || n