[Contest on 2021.9.7] 睡着了,但不完全睡着了

Posted 十步

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了[Contest on 2021.9.7] 睡着了,但不完全睡着了相关的知识,希望对你有一定的参考价值。

\\(\\text{Strange Queries}\\)

解法

首先有这样的转移:

\\[\\begin{cases}f(n,0)=f(n-1,1) \\\\\\displaystyle f(n,1)=\\lvert a_n-a_{n-1}\\rvert+\\min\\{f(n-1,0),f(n-1,1)\\}\\end{cases} \\]

其中 \\(0/1\\) 表示 \\(n\\) 否/是与 \\(n-1\\) 建立关系。但这题需要我们进行区间合并,这个 \\(\\mathtt{dp}\\) 提醒我们从两个方向来思考:维护 \\(\\mathtt{dp}\\) 转移方式(矩阵);改写一个可以区间合并的 \\(\\mathtt{dp}\\)

这里只讲区间合并的 \\(\\mathtt{dp}\\)。类似地,我们在区间 \\([l,r]\\) 上维护四个变量:\\(l,r\\) 两个端点都建立关系的最小值;\\(l,r\\) 两个端点中,\\(l\\)\\(r\\) 可以不 建立关系的最小值;\\(l,r\\)可以不 建立关系的最小值。这四个变量是一层一层想到的。另外,需要注意 "可以不" 并不是 "不",这导致转移时可以不考虑某些情况,因为已经被包含在限制较小的变量中了。

具体就用一棵平衡树维护,还需要维护区间 \\(\\max,\\min\\) 来表示 \\(\\lvert a_n-a_{n-1}\\rvert\\)。最开始建树的时候要先排序。

代码

#include <cstdio>
#define print(x,y) write(x),putchar(y)

template <class T>
inline T read(const T sample) {
	T x=0; char s; bool f=0;
	while((s=getchar())>\'9\' or s<\'0\')
		f|=(s==\'-\');
	while(s>=\'0\' and s<=\'9\')
		x=(x<<1)+(x<<3)+(s^48),
		s=getchar();
	return f?-x:x;
}

template <class T>
inline void write(const T x) {
	if(x<0) {
		putchar(\'-\');
		write(-x);
		return;
	}
	if(x>9) write(x/10);
	putchar(x%10^48);
}

#include <cstdlib>
#include <iostream>
#include <algorithm>
using namespace std;
typedef long long ll;

const int maxn=2e5+5;

int n,q,idx,rt;
struct node {
	int mn,mx,siz,ls,rs,key,val;
	ll ans,bl,br,ba;
	
	void Put() {
		printf("VALUE %d %d %d\\n",mn,mx,val);
		printf("%lld %lld %lld %lld\\n",ba,bl,br,ans);
	}
} t[maxn];

int NewNode(int x) {
	t[++idx].siz=1;
	t[idx].ls=t[idx].rs=0;
	t[idx].key=rand();
	t[idx].siz=1;
	t[idx].val=t[idx].mn=t[idx].mx=x;
	t[idx].ba=t[idx].bl=t[idx].br=0;
	t[idx].ans=1e15;
	return idx;
}

void Update(const node &a,const node &b,node &o) {
	node t=o; int inc=b.mn-a.mx;
	t.ba=min(a.bl+b.br,a.ba+b.ba+inc);
	t.bl=min(a.bl+b.ans,a.ba+b.bl+inc);
	t.br=min(a.ans+b.br,a.br+b.ba+inc);
	t.ans=min(a.ans+b.ans,a.br+b.bl+inc);
	o=t;
}

void pushUp(int o) {
	if(!o) return;
	int l=t[o].ls,r=t[o].rs;
	t[o].siz=t[l].siz+t[r].siz+1;
	t[o].mn=t[o].mx=t[o].val;
	t[o].ba=t[o].bl=t[o].br=0;
	t[o].ans=1e15;
	if(l) Update(t[l],t[o],t[o]),t[o].mn=t[l].mn;
	if(r) Update(t[o],t[r],t[o]),t[o].mx=t[r].mx;
}

void split(int o,int k,int &x,int &y) {
	if(!o) x=y=0;
	else {
		if(t[o].val<=k)
			x=o,
			split(t[o].rs,k,t[o].rs,y);
		else
			y=o,
			split(t[o].ls,k,x,t[o].ls);
		pushUp(o);
	}
}

int merge(int x,int y) {
	if(!x or !y) return x|y;
	if(t[x].key<t[y].key) {
		t[x].rs=merge(t[x].rs,y);
		pushUp(x);
		return x;
	}
	else {
		t[y].ls=merge(x,t[y].ls);
		pushUp(y);
		return y;
	}
}

int Val[maxn];

int main() {
	srand(20210907);
	int a,b,c; 
	for(int T=read(9);T;--T) {
		n=read(9),q=read(9);
		rt=idx=0;
		for(int i=1;i<=n;++i)	
			Val[i]=read(9);
		sort(Val+1,Val+n+1);
		for(int i=1;i<=n;++i)
			rt=merge(rt,NewNode(Val[i]));
		while(q--) {
			int x,y;
			x=read(9),y=read(9);
			if(x==1) {
				split(rt,y-1,a,b);
				rt=merge(merge(a,NewNode(y)),b);
			}
			else {
				split(rt,y-1,a,b);
				split(b,y,b,c);
				b=merge(t[b].ls,t[b].rs);
				rt=merge(merge(a,b),c);
			}
			print(t[rt].ans>1e14?0:t[rt].ans,\'\\n\');
		}
	}
	return 0;
}

\\(\\text{[TJOI 2013] }\\)拯救小矮人

解法

首先将小矮人按照 \\(a_i+b_i\\) 从小到大排序。为什么捏?不妨令 \\(H-(a_i+b_i)\\) 是第 \\(i\\) 个小矮人的逃生高度,对于相邻小矮人 \\(i,j\\),如果我们期望 两个小矮人都逃出去,那么放逃生高度越大的小矮人越优。

但问题是有可能小矮人不逃出去更优,可以令 \\(dp_i\\) 为逃出 \\(i\\) 个小矮人后可以垒出的最大高度,这是一个 \\(\\mathcal O(n^2)\\)\\(\\mathtt{dp}\\)

更优的做法是反悔贪心。先依次使可以逃出的小矮人逃出,并用优先队列记录它的 \\(a\\),到某个无法逃出的小矮人 \\(i\\) 时,我们考虑这样操作:找到队列中最大的 \\(a\\),如果踢回这个小矮人能使 \\(i\\) 逃出,且 \\(\\max a>a_i\\),我们就把这个小矮人 \\(j\\) 换成 \\(i\\)

首先 \\(j\\)\\(i\\) 这段的小矮人肯定是不受影响的,如果 \\(j\\) 能够使之间的小矮人逃出就早换了。对于之后的小矮人,垒出的高度变高了,解也就变优了。这是 \\(\\mathcal O(n\\log n)\\) 的。

代码

#include <cstdio>
#define print(x,y) write(x),putchar(y)

template <class T>
inline T read(const T sample) {
	T x=0; char s; bool f=0;
	while((s=getchar())>\'9\' or s<\'0\')
		f|=(s==\'-\');
	while(s>=\'0\' and s<=\'9\')
		x=(x<<1)+(x<<3)+(s^48),
		s=getchar();
	return f?-x:x;
}

template <class T>
inline void write(const T x) {
	if(x<0) {
		putchar(\'-\');
		write(-x);
		return;
	}
	if(x>9) write(x/10);
	putchar(x%10^48);
}

#include <queue>
#include <algorithm>
using namespace std;

const int maxn=2005;

int n;
struct node {
	int a,b;
	
	bool operator < (const node &t) const {
		return a+b<t.a+t.b;
	} 
} s[maxn];
priority_queue <int> q; 

int main() {
	n=read(9);
	for(int i=1;i<=n;++i)
		s[i]=(node){read(9),read(9)};
	sort(s+1,s+n+1);
	int h=read(9);
	long long sum=0; int ans=0;
	for(int i=1;i<=n;++i)
		sum+=s[i].a;
	for(int i=1;i<=n;++i)
		if(sum+s[i].b>=h) {
			sum-=s[i].a,++ans;
			q.push(s[i].a);
		}
		else {
			if(!q.empty() and sum+q.top()+s[i].b>=h and q.top()>s[i].a) {
				sum+=q.top()-s[i].a;
				q.pop();
				q.push(s[i].a);
			}
		}
	print(ans,\'\\n\');
	return 0;
}

\\(\\text{[ICPC World Finals 2019] Hobson }\\)的火车

题目描述

给你多棵内向基环树,对于每个点,求出有多少个点走 \\(\\le k\\) 步能走到这个点。

解法

感觉自己的脑子里根本不存在差分这个东西,好几次想用树链剖分硬艹了…

先找出环,\\(\\rm dfs\\) 每个环上点的子树,用一个栈动态维护这个节点到根的节点,这样就可以找到走 \\(k+1\\) 才能走到的那个祖先。

然后考虑点对环的贡献。考虑破环为链,随便选一个点作为 \\(rt_0\\)(链的起点)。如果能贡献整个环,就将 \\(rt_0\\) 的差分数组加一,反之计算出它在环上贡献的一段区间 \\([l,r]\\),需要注意有 \\(l>r\\) 的情况,这时需要将 \\(rt_0\\) 的差分数组加一。

代码

#include <cstdio>
#define print(x,y) write(x),putchar(y)

template <class T>
inline T read(const T sample) {
	T x=0; char s; bool f=0;
	while((s=getchar())>\'9\' or s<\'0\')
		f|=(s==\'-\');
	while(s>=\'0\' and s<=\'9\')
		x=(x<<1)+(x<<3)+(s^48),
		s=getchar();
	return f?-x:x;
}

template <class T>
inline void write(const T x) {
	if(x<0) {
		putchar(\'-\');
		write(-x);
		return;
	}
	if(x>9) write(x/10);
	putchar(x%10^48);
}

#include <vector>
using namespace std;

const int maxn=5e5+5; 

int n,k,f[maxn],ans[maxn];
int vis[maxn],stk[maxn],tp;
int dep[maxn],siz;
vector <int> e[maxn],rt;

void dfs(int u,int root) {
	if(!vis[u]) vis[u]=1;
	stk[++tp]=u;
	if(tp>k+1)
		if(stk[tp-k-1]^rt[root])
			--ans[stk[tp-k-1]];
	for(auto v:e[u]) {
		if(vis[v]==2) continue;
		dep[v]=dep[u]+1;
		dfs(v,root);
		if(u^rt[root])
			ans[u]+=ans[v];
	}
	--tp;
	if(dep[u]-1<=k) {
		if(dep[u]-1+siz>k+1) {
			++ans[rt[root]];
			--ans[rt[(root+k-dep[u]+2+siz)%siz]];
			if((root+k-dep[u]+2+siz)%siz<=root)	
				++ans[rt[0]];
		}
		else ++ans[rt[0]];
	}
	if(rt[root]^u) ++ans[u];
}

void work(int x) {
	rt.clear();
	while(vis[x]^2) {
		if(vis[x])
			rt.push_back(x);
		++vis[x]; x=f[x];
	}
	siz=rt.size();
	for(int i=0;i<rt.size();++i)
		dep[rt[i]]=1,dfs(rt[i],i);
	for(int i=1;i<rt.size();++i)
		ans[rt[i]]+=ans[rt[i-1]];
}

int main() {
	n=read(9),k=read(9);
	for(int i=1;i<=n;++i) {
		f[i]=read(9);
		e[f[i]].push_back(i);
	}
	for(int i=1;i<=n;++i)
		if(!vis[i]) work(i);
	for(int i=1;i<=n;++i)
		print(ans[i],\'\\n\');
	return 0;
}

简单题

题目描述

有两个长度为 \\(K\\) 的数列 \\(a,b\\),满足 \\(\\sum a_i=N,\\sum b_i=M\\)

定义权值 \\(P\\)

\\[\\prod \\min\\{a_i,b_i\\} \\]

你需要求出所有可能数列组合的权值之和。取模 \\(998244353\\)

多组数据,\\(T\\le 100,N,M,K\\le 5\\cdot 10^5\\)

解法

定义 \\(dp_{i,j,k}\\) 为在第 \\(i\\) 位,数列 \\(a\\) 数字和为 \\(j\\),数列 \\(b\\) 数字和为 \\(k\\) 的权值之和。那么有:

\\[dp_{i,j,k}=dp_{i,j,k}+dp_{i-1,j-x,k-y}\\cdot \\min\\{x,y\\} \\]

这是 \\(\\mathcal O(n^4k)\\) 的。具体优化可以枚举 \\(\\min\\{x,y\\}\\),后两维的前缀和即可,应该还要再容斥一下,就有 \\(\\mathcal O(n^3k)\\) 了。


我们可以思考权值的组合意义 —— 定义 \\(\\{c\\}\\) 满足 \\(\\forall i,c_i\\le \\min\\{a_i,b_i\\}\\),那么对于固定的 \\(\\{a\\},\\{b\\}\\),所有合法的 \\(\\{c\\}\\) 的个数即为权值!

转化一下,对于每种 \\(\\{c\\}\\),计算 \\(\\{a\\},\\{b\\}\\) 的种类数。

枚举 \\(s=\\sum c_i\\),为了保证条件成立,可以先令 \\(c_i=a_i=b_i\\),再将 \\(N-s\\)\\(M-s\\) 填入 \\(K\\) 个位置(插板法),就有:

\\[\\text{Ans}=\\sum_{s=0}^{\\min\\{N-K,M-K\\}}\\binom{s+K-1}{K-1}\\cdot \\binom{N-s-1}{K-1}\\cdot \\binom{M-s-1}{K-1} \\]

注意初始时先将 \\(1\\) 填入每一个位置,所以后面两个组合数的上面本来还要加个 \\(K\\),就抵消掉了。

以上是关于[Contest on 2021.9.7] 睡着了,但不完全睡着了的主要内容,如果未能解决你的问题,请参考以下文章

柴油中的显式 JOIN ON

atcoder NIKKEI Programming Contest 2019 E - Weights on Vertices and Edges

German Collegiate Programming Contest 2018? A. Attack on Alpha-Zet

AtCoder Regular Contest 063 E:Integers on a Tree

NIKKEI Programming Contest 2019-2 Task D. Shortest Path on a Line

Codeforces Round #625 (Div. 2, based on Technocup 2020 Final Round) A. Contest for Robots(思维题)