codeforces round #695 (Div2) A~E题解

本场链接:Codeforces Round #695 (Div. 2)

A. Wizard of Orz

题目大意

\(n\)个灯,每个灯一开始都是0,每一秒钟每个灯显示的数字会往上一位,但是由于每个灯只能显示单个数位,所以从\(9\)增大时得到的是\(0\).在任意时刻,你可以指定唯一一个灯被停止,下一秒后,他相邻的两个灯也会进入暂停状态.问,在合理的设置一个灯被停止之后,所有灯停止,显示的最大的数是多少.

注:先增加数位,后停止.

数据范围:

\(1 \leq n \leq 2*10^5\)

思路

显然应该是从高位到低位尽量最大,一个很直接但是不对的想法是,直接让最高位就是9,后一位是8,接着往后递推,依次减少.不对的原因是如果让第二位是8的时候被停止,可以使前三位是\(989\),总体更大.后面递推即可.

代码

#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
#define forn(i,x,n) for(int i = x;i <= n;++i)	


int main()
{
    int T;scanf("%d",&T);
    while(T--)
    {
		int n;scanf("%d",&n);
		int cur = 9;
		printf("9");
		if(n >= 2)	printf("8");
		forn(i,3,n)
		{
			printf("%d",cur);
			if(cur == 9)	cur = 0;
			else	++cur;
		}
    	puts("");
    }
    return 0;
}

B. Hills And Valleys

题目大意

对于任意一个\(j \in [2,n-1]\)来说如果他满足\(a_j\)严格小于身边两个元素,或者\(a_j\)严格大于身边两个元素,分别称为波谷和波峰.令一个数组\(a\)的牛逼值是整个数组中波谷和波峰的数量之和,现在你可以修改数组中任意一个元素,使他变成任意一个值,问这个数组最小的牛逼值是多少.

数据范围:

\(1 \leq n \leq 3 *10^5\)

\(1 \leq a_i \leq 10^9\)

思路

首先肯定统计一下原来数组的波谷波峰的个数,随便捣鼓一下就算完了,记为\(sum\).

考虑如何做这个修改:这是一个非常显然的套路,因为只能修改一个数,所以直接枚举每一个数被修改的情况即可,进而可以想到,加入要修改\(a_i\)那么最好的修改策略就是让\(a_i = a_{i-1}\)或者\(a_i = a_{i + 1}\)因为这样简单又能消灭波峰和波谷,之后我们直接枚举每一位被修改的情况就可以了.

首先从\(sum\)里面挖掉与\(i\)相关的,构成三元组的部分,其次把\(a_i\)修改,统计新的三元组的贡献,枚举获得最小值即可.

代码

#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
#define forn(i,x,n) for(int i = x;i <= n;++i)	

const int N = 3e5+7;
int up[N],dw[N],a[N],n;
int calc(int i)
{
	if(i <= 1 || i >= n)	return 0;
	int res = 0;
	if(a[i] > a[i - 1] && a[i] > a[i + 1])	++res;
	if(a[i] < a[i - 1] && a[i] < a[i + 1])	++res;
	return res;
}
int main()
{
    int T;scanf("%d",&T);
    while(T--)
    {
		scanf("%d",&n);
		forn(i,1,n)	scanf("%d",&a[i]),up[i] = 0,dw[i] = 0;
		up[0] = dw[0] = up[n + 1] = dw[n + 1] = 0;
		int res = 1e9,sum = 0;
		forn(i,2,n - 1)	sum += calc(i);
		res = sum;

		forn(i,1,n)
		{
			int tmp = sum - calc(i) - calc(i - 1) - calc(i + 1);
			int p = a[i];
			a[i] = a[i - 1];
			tmp += calc(i) + calc(i - 1) + calc(i + 1);
			a[i] = p;
			
			res = min(res,tmp);
			
			tmp = sum - calc(i) - calc(i - 1) - calc(i + 1);
			a[i] = a[i + 1];
			tmp += calc(i) + calc(i - 1) + calc(i + 1);
			a[i] = p;
			
			res = min(res,tmp);
		}
		printf("%d\n",res);
    }
    return 0;
}

C. Three Bags

题目大意

有三个可重集,分别给定这三个集合的大小和里面的元素.现在定义一种操作:分别指定两个非空集合A\B,从A里面取出一个元素\(a\),从B里面取出一个元素\(b\),把\(b\)B里面删除,把A集合中的\(a\)替换成\(a-b\).经过若干次操作之后,所有集合里面的元素个数会只剩下一个,求这个剩下的数的最大值.

数据范围:

\(1 \leq n1,n2,n3 \leq 3 * 10^5\)

\(1 \leq n1 + n2 + n3 \leq 3 * 10^5\)

\(1 \leq a \leq 10^9\)

思路

稍微有点抽象的一题,最好模拟一下过程.

通过模拟过程我们可以发现这个题想要最大的值加和进去,应该先让一个集合里的最小值去减掉外面一个集合的值,再把需要合并的数拿去减这个最小值减外面的值剩下的结果,这样的话,这个合并的结果就是原来的数加上外面的集合的一个值并且减去另外一个集合里面的最小值.用形式化的语言来说明,设三个集合\(A,B,C\),三个元素对应\(a,b,c\).假设我们现在想合并这三个数到\(a\)头上,并且\(c\)是比\(b\)小的,那么可以先让\(c-b\),之后让\(a-(c-b)\),这样的话我们就让\(b\)加到\(a\)头上了,但是需要支付\(-c\)的代价,那么什么时候\(a\)最大呢,显然应该是\(c\)取最小值的时候.但是这里有个问题,对于和\(a\)元素在同一个集合里的元素如何统计?我们可以发现对于同一个集合的元素只要外面存在元素,那么一定可以让外面的直接减掉再加回到原来的集合里去,这里我们直接认为整个\(A\)集合是合并好的就可以了(因为初始情况不会有集合是空的).

想到这里大概的就可以明确一下题目做法了:枚举每个集合作为被合并的集合,对于被合并的集合自己来说,他自己集合里的所有元素是可以直接合并的,对于其他两个集合的元素来说,他们需要一个最小值来作为跳板才能进入\(A\)集合,现在来考虑这个跳板的问题:经过前面的分析不难想到跳板其实有两种情况,一种是\(B\)\(C\)里面都有一个最小值作为跳板,那么他们两个集合中只要不是最小值的元素(如果有多的,那么就认为只有其中一个被拿来作为跳板,其他的都可以拿去合并)都可以直接去合并,合并之后需要支付的代价就是两个最小值;还有一种就是我只有一个跳板,例如所有的\(B\)及和里面的元素都通过\(C\)集合合并到\(A\),这种情况下\(C\)集合里没有任何一个元素是跳了一次再进入\(A\)的,所以代价就是整个\(C\)集合的负数和.

综上,剩下的就是枚举每个集合以及有两个跳板和一个跳板的情况了.最终找出最大值即可.

不过需要注意一下,我的代码思路就是先求和,求和的时候要先挖掉本来的贡献,再去支付代价,比赛时就是这里没考虑清楚没算到结果.

#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
#define forn(i,x,n) for(int i = x;i <= n;++i)	

const int N = 3e5+7;
int n[3];
int a[3][N];
ll sum[3],minv[3];
int main()
{
	scanf("%d%d%d",&n[0],&n[1],&n[2]);
	forn(i,0,2)	forn(j,1,n[i])	scanf("%d",&a[i][j]);
	forn(i,0,2)
	{
		minv[i] = a[i][1];
		forn(j,1,n[i])
		{
			sum[i] += a[i][j];
			minv[i] = min(minv[i],1ll*a[i][j]);
		}
	}
	
	ll res = 0,all = sum[0] + sum[1] + sum[2];
	// cout << all << endl;
	// cout << minv[0] << " " << minv[1] << " " << minv[2] << endl;
	res = max(res,all - 2 * minv[0] - 2 * minv[1]);
	res = max(res,all - 2 * minv[0] - 2 * minv[2]);
	res = max(res,all - 2 * minv[1] - 2 * minv[2]);
	
	res = max(res,all - 2 * sum[0]);
	res = max(res,all - 2 * sum[1]);
	res = max(res,all - 2 * sum[2]);

	printf("%lld",res);
    return 0;
}

D. Sum of Paths

题目大意

有一个\(n\)个格子的横直条,一开始有个机器人,位置随意.每一秒他可以向左或者向右走一格,但是不可以跨过边界.任何一个长度是\(k\)的路径,只要没有走出格,就认为是合法的一种路线.对于每个格子都有一个权值,当机器人走到上面的时候,得分就会增加对应的权值.问所有合法的路径得到的分数的总和是多少,对\(10^9+7\)取模.

注意:权值可以多次计算.只要经过了就算.

例子:3个格子可以走4步,那么(1,2,3,4) \ (1,2,1,2)都算是合法的路线.

接下来有\(q\)个询问,每个询问给定两个值\(i,x\)表示将\(a_i\)修改成\(x\).每个询问之后输出修改后的总和.

数据范围:

\(2 \leq n \leq 5000\)

\(1 \leq k \leq 5000\)

$1 \leq q \leq 2 * 10^5 $

思路

看到单点修改的询问就可以想到这肯定是一个套路的维护,考虑每个点上的\(a_i\)对整个和的贡献.

\(cnt_i\)表示第\(i\)个格子在所有合法的路线中出现了几次,那么显然答案就是\(\sum_{i=1}^ncnt_i*a_i\),只要我们能求出\(cnt_i\)对于后面的每个询问,只需要把一开始的贡献删掉,再加上修改之后新的贡献就可以了.

剩下的问题就是如何求\(cnt_i\),经过模拟找规律之后可以发现大概是很难找规律的,因为他会有很多往回走的情况,不过题目的数据范围启发了\(dp\)的思路,一个比较套路的但是还不知道又什么用的\(dp\)做法是:\(f[i][j]\)表示当前在\(i\)这个位置,已经走了\(j\)步的合法方案个数.状态转移方程比较简单这里就不展开了\(f[i][j] = f[i - 1][j - 1] + f[i + 1][j - 1]\)\(i=1\)\(i = n\)的情况下特判.这个信息暂时还想不到跟\(cnt_i\)有什么直接的联系,先放放.

接下来我们考虑能不能从\(cnt_i\)身上找点信息逆推回去,因为\(cnt_i\)是表示所有合法的路径中\(i\)编号出现的个数,那么我们可以通过走了多少步来划分\(cnt_i\)的来源,也就是说我们去看所有合法的长度是\(k\)的路径,枚举\(j\in[0,k]\),第\(j\)位是\(i\)的方案的个数,把这些方案累加起来就是出现\(i\)的次数.这里另设一个状态\(g[i][j]\):

  • \(g[i][j]\)表示在所有的合法路径中编号\(i\)在出现在\(j\)位的次数.
  • 显然根据上面的划分来说\(cnt_i = \sum_{i=0}^k g[i][j]\).
  • 入口:初始全为\(0\)即可
  • 转移:整个长度为\(k\)的路径,第\(j\)为出现的是\(i\),那么前面\(j\)步的方案数恰好就是走了\(j\)步走到\(i\)的方案数即\(f[i][j]\),对于后面\(k-j\)步,我们可以从反方向考虑,某个状态走了\(k-j\)步走到了\(i\),也就是\(f[i][k - j]\),那么转移方程就是\(g[i][j] = f[i][j] * f[i][k - j]\)

做完\(g\)的递推之后,求一下\(cnt\)即可.剩下的询问就非常简单了,不做多过多解释.

#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
#define forn(i,x,n) for(int i = x;i <= n;++i)	

const int N = 5005,MOD = 1e9+7;
ll f[N][N],cnt[N],g[N][N];
int a[N];
int main()
{
	int n,k,q;scanf("%d%d%d",&n,&k,&q);
	forn(i,1,n)	scanf("%d",&a[i]);
   	forn(i,1,n)	f[i][0] = 1;
   	
   	forn(j,1,k)	forn(i,1,n)	
   		f[i][j] = (i >= 2 ? f[i - 1][j - 1] : 0) + (i <= n - 1 ? f[i + 1][j - 1] : 0),f[i][j] %= MOD;
	forn(i,1,n)	forn(j,0,k)	g[i][j] = (g[i][j] + f[i][j] * f[i][k - j] % MOD) % MOD;
	
   	forn(i,1,n)	forn(j,0,k)	cnt[i] = (cnt[i] + g[i][j]) % MOD;
   	// forn(i,1,n)	printf("%lld ",cnt[i]);puts("");
   	
   	ll ori = 0;
   	forn(i,1,n)	ori = (ori + cnt[i] * a[i] % MOD) % MOD;
   	
   	while(q--)
   	{
   		int i,x;scanf("%d%d",&i,&x);
   		ori = (ori - cnt[i] * a[i]) % MOD;
   		ori = (ori + MOD) % MOD;
   		a[i] = x;
   		ori = (ori + cnt[i] * a[i] % MOD) % MOD;
   		printf("%lld\n",ori);
   	}
    return 0;
}

E. Distinctive Roots in a Tree

题目大意

给定一个\(n\)个点的树,定义一个牛逼的点当且仅当从这个点走出去的任意一条路径(包括起点自己)中都不存在相同的权值的点对.求树中牛逼的点的个数.

数据范围:

\(1 \leq n \leq 2 * 10^5\)

\(1 \leq a_i \leq 10^9\)

思路

首先比较显然的是需要统计权值的个数,一开始要么离散化一下要么扔map统计.总之预处理出一个\(cnt\)表示权值为\(i\)的点数.

预处理完了之后考虑这个题本身应该怎么做:考虑某个点\(u\)以及他的儿子\(c\).记\(a_i\)为点\(i\)的颜色,那么如果以\(c\)为根的子树里存在某个点与\(a_u\)相同,那么说明所有可能是牛逼点的点应该属于以\(c\)为根的子树的集合里,因为对于其他的点来说,一定会有一条路走\(u\)再走\(c\)里面的某个点,除非你把这两个相同颜色的点切开,使得两个点在各一边才行.

如果下面分析看不明白,建议具体化一下图,以及看一下后面的总述

那么这就是一种情况:假如\(u\)的某个儿子里存在一个点与之同色,那么所有牛逼的点应该属于这个儿子的子树集合.当然这里并不明确是谁,这个不重要,因为这个过程是一个往下递归的过程,我们之后再来考虑下面的同色的点的情况.这里应该对整颗子树\(c\)打一个标记表示牛逼点可能是他们,还要对所有其他的点打一个标记表示牛逼的点绝对不是他们了.这里的处理方式也有多种,我的是对整颗树打一个\(+1\)的标记,对子树打一个\(-1\)的标记,只有标记值为\(0\)的点表示是一个牛逼的点.

当然讨论到这里肯定还没完,因为如果我们只有上面这种对整颗树打正一,对需要修改的子树打负一的标记的话,从\(c\)往下递归,走到那个与\(u\)同色的点的时候,我们并不会"向上看",但是实际上我们要把这个同色点(记作\(z\))以下的部分也切断,尽管他下面可能没有同色的点了,但是因为外面存在的\(u\)的原因,对于\(z\)以下的点往上走一定会有一条\(z->c->u\)的路线导致同色.所以这里还需要讨论一种情况就是,如果统计完了某颗子树\(u\),但是在\(u\)之外的点中还存在与\(u\)相同的颜色的点,那么\(u\)为根的子树也必须要抛弃掉(即打一个正一标记).

总的来说,这个过程就是看\(u\)下的儿子\(c\)里的子树是否存在一个\(z\),且\(a_u=a_z\),如果存在的话,只有\(c\)\(z\)这部分的点是被夹住的,通过分析我们可以知道只有被夹住的点才不会出现相同颜色的路径.那么打标记的意图就是使的只有被夹住的部分标记是\(0\),先对所有点打一个\(+1\),其次只有点\(c\)以下的子树打\(-1\),此时点\(c\)以下的子树所有点都是\(0\).我们应该对点\(z\)也打上一个标记\(1\),但是我们并不知道他具体在哪里,所以我们先向下递归,走到每个点的时候"向上看",看上面之外的地方是否存在同色的点,如果存在就把这颗子树抛弃掉(打标记\(1\)).

接下来考虑怎么维护,要么树上差分,要么就用\(dfs\)序差分.这里就不展开了.就是一个子树标记的问题.

#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
#define forn(i,x,n) for(int i = x;i <= n;++i)	

const int N = 2e5+7,M = 2 * N,LIM = 18;
int edge[M],succ[M],ver[N],idx;
int depth[N],fa[N][LIM + 3];
int a[N],cnt[N],dwcnt[N],sum[N];
int dfn[N],time_stamp,siz[N],n;
vector<int> val;
inline int link(int x)
{
	return lower_bound(val.begin(),val.end(),x) - val.begin();
}

void cover(int l,int r,int v)
{
	if(l > r)	return ;
	sum[l] += v;
	sum[r + 1] -= v;
}

void add(int u,int v)
{
	edge[idx] = v;
	succ[idx] = ver[u];
	ver[u] = idx++;
}

void dfs(int u,int fa)
{
	dfn[u] = ++time_stamp;	siz[u] = 1;
	int allu = dwcnt[a[u]];
	++dwcnt[a[u]];
	for(int i = ver[u];~i;i = succ[i])
	{
		int v = edge[i];
		if(v == fa)	continue;
		int other = dwcnt[a[u]];
		dfs(v,u);
		if(dwcnt[a[u]] > other)
		{
			cover(1,n,1);
			cover(dfn[v],dfn[v] + siz[v] - 1,-1);
		}
		siz[u] += siz[v];
	}
	allu = dwcnt[a[u]] - allu;
	if(allu < cnt[a[u]])	cover(dfn[u],dfn[u] + siz[u] - 1,1);
}

int main()
{
	memset(ver,-1,sizeof ver);
	scanf("%d",&n);
	
	forn(i,1,n)	scanf("%d",&a[i]),val.push_back(a[i]);
	sort(val.begin(),val.end());
	val.erase(unique(val.begin(),val.end()),val.end());
	forn(i,1,n)	a[i] = link(a[i]),++cnt[a[i]];
	
	forn(i,1,n - 1)
	{
		int u,v;scanf("%d%d",&u,&v);
		add(u,v);add(v,u);
	}
	
	dfs(1,-1);
	
	forn(i,1,n)	sum[i] += sum[i - 1];
	int res = 0;
	forn(i,1,n)	res += (sum[i] == 0);
	printf("%d",res);
	
    return 0;
}

posted @ 2021-01-09 14:51  随处可见的阿宅  阅读(450)  评论(0编辑  收藏  举报