Codeforces Round 958 (Div. 2)

前言

        没错,这场打得稀烂,A 题一个小时都没切,掉下浅绿名......

        Standings:7955

        题目链接:Dashboard - Codeforces Round 958 (Div. 2) - Codeforces

A. Split the Multiset

        我愿称之为目前做过的最难的 A 题!(其实 A 能难过 BCD 吗?可能是比赛的时候短路了一直往错的贪心想)

        题意:

        给一个只含有一个元素 n 的多重集合,需要你进行若干次操作,每次操作选择一个集合里的一个元素 u ,再向集合里加入不超过 k 个元素,使这些加入的元素之和等于 u 。最后要求得到的多重集合是 n 个 1 ,求最小的操作次数。

        思路:

        一眼贪心,很像合并果子之类的题目,首先显然的贪心是当 u >= k 的时候,一定要加满 k 个元素,这样不会导致浪费操作,下面考虑加入的这些元素要如何分配。其实只需尽可能多地加入“1” 就是最优方案。

        第一种解释:我们最后要得到的全为 “1” 的多重集合,这样可以保证我们每一次操作得到最多的 “1” ,可以让我们以最快的速度达成目标。

        第二种解释:无论如何分配这不超过 k 个元素,要减少的总数值都是(n - 1),分配尽可能多的 “1” 是无须进行二次操作的,而如果分配的数里有两个或以上不为 1 的数,这些数需要单独二次操作,这样每个数二次操作可能用不满 k 个元素,这样就导致了更多的操作数浪费。

#include<cstdio>
#include<cstring>
using namespace std;

int T,n,k;

int ask(int x,int y)
{
	if(x == 1) return 0;
	if(x <= y) return 1;
	return ask(x - (k - 1),y) + 1;
}

int main()
{
	scanf("%d",&T);
	while (T --)
	{
		scanf("%d%d",&n,&k);
		printf("%d\n",ask(n,k));
	}
	return 0;
}

B. Make Majority

        题意:

        给一个长度为 n 的 01 序列 a,每次操作选取一段区间 [l,r] ,记 eq?c_0 表示这段区间内 0 的数目,记 eq?c_1 表示这段区间内 1 的数目。

        1. eq?c_0 >= eq?c_1 ,则将这段区间变成单个元素 0

        2. eq?c_0  <  eq?c_1 ,则将这段区间变成单个元素 1

        询问对于每个给定的 01 串,能否通过若干次操作使最终的序列全是 1 。

        思路:

        简单来说就是要消灭所有的 0 ,显然我们可以先把所有长度大于 1 的连续的 0 都缩成一个单个的 0 ,那么此时原数列就变成了若干个单个的 0 嵌在一堆 1 中间,下面我们考虑对这些 0 逐个击破。对于每个 0 ,消灭它需要身边有至少两个 1 (“011”、“101”、“110”),每次我们选取这样连续的三个数并把它变成 1 ,发现每次操作都是减少了一个 1 和一个 0 。于是我们只需判断处理了连续 0 之后的序列中,eq?c_1 >= eq?c_0 + 1 是否成立即可。若成立,我们每次都一定可以找到至少一个这样的三个数并击破,最后得到全为 1 的串。

#include<cstdio>
#include<cstring>
using namespace std;

#define N 200005

int T,n,a[N];
char s[N];

int main()
{
	scanf("%d",&T);
	while (T --)
	{
		scanf("%d",&n);
		scanf("%s",s + 1);
		for (int i = 1;i <= n;++ i) a[i] = s[i] - '0';
		int x,y;
		x = y = 0;
		if(a[1] == 1) x = 1;
		else y = 1;
		for (int i = 2;i <= n;++ i)
			if(a[i] != a[i - 1])
			{
				if(a[i] == 1) ++ x;
				else ++ y;
			}
			else if(a[i] == 1) ++ x;
		if(x >= y + 1) printf("Yes\n");
		else printf("No\n");
	}
	return 0;
}

C. Increasing Sequence with Fixed OR

        题意:

        给定一个正整数 n ,构造出一个满足以下条件的最长序列。

        1. eq?%5Cforall  2<=i<=k ,eq?a_i <= n

        2. 序列 a 严格递增

        3. eq?%5Cforall  2<=i<=k ,eq?a_i%20%7C%20a_%7Bi-1%7D = n

        思路:

        我们从大到小构造序列,那么显然最大的那个数可以构造为 n ,考虑往下如何构造。

        由于要使相邻两个数异或和为 n ,我们又是从大到小构造的,于是每一步的最优方案就是 n 从小到大依次减去二进制下的 1 。

        我们以 23 为例,在二进制下观察:

        1 0 1 1 1 (23)

        1 0 1 1 0 (22)

        1 0 1 0 1 (21)

        1 0 0 1 1 (19)

        0 0 1 1 1  (7)

        因此假设 n 的二进制下有 m 个 1 ,则最长序列的长度为 m + 1 。

#include<cstdio>
#include<cstring>
using namespace std;

int T,m;
long long pow[62],n,a[62];

int main()
{
	pow[0] = 1ll;
	for (int i = 1;i <= 61;++ i) pow[i] = pow[i - 1] << 1;
	scanf("%d",&T);
	while (T --)
	{
		scanf("%lld",&n),m = 0;
		long long now = n;
		for (int i = 61;i >= 0;-- i)
			if(now >= pow[i]) a[++ m] = pow[i],now -= pow[i];
		if(m == 1)
		{
			printf("1\n");
			printf("%lld\n",n);
			continue;
		}
		printf("%d\n",m + 1);
		for (int i = 1;i <= m;++ i) printf("%lld ",n - a[i]);
		printf("%lld\n",n);
	}
	return 0;
}

D. The Omnipotent Monster Killer

        题意:

        给一棵 n 个节点的树,每个节点上都有一个怪兽,每个怪兽都有一个攻击值 。每一轮所有存活的怪兽都会对你产生攻击,之后你需要在每一轮消灭若干怪兽,被消灭的怪兽在树上不能相邻,直到所有的怪兽都被消灭,求这个过程中怪兽产生的最小攻击值总和。

        思路:

        一眼树形dp,而且很像最经典的那道题 —— “ 没有上司的舞会 ” 。考场时飞速码完了但一直 WA,造成了这次 rating 的惨剧。

        设 eq?f_%7Bi%2Cj%7D 表示节点 i 上的怪兽在第 j 轮被消灭时,以 i 为根的子树内总攻击值的最小值,那么它本身的贡献就是 eq?a_i * j ,在转移的时候从子节点上的怪兽都不在第 j 轮消灭来转移即可。

        时间复杂度:不用前缀和优化就是 O( eq?n%7Blog%7D%5E2n ),前缀和优化就是 O(nlogn)。

#include<cstdio>
#include<cstring>
using namespace std;

#define N 300005
#define Inf 8e18

int T,n,st[N],cnt;
long long a[N],f[N][22],ans,pre[N][22],suf[N][22];

struct Edge
{
	int next,to;
}ed[N << 1];

long long min(long long x,long long y) { return x < y ? x : y ; }

void add(int u,int v)
{
	ed[++ cnt].next = st[u];
	ed[cnt].to = v;
	st[u] = cnt;
	return;
}

void dfs(int x,int fa)
{
	for (int i = 1;i <= 20;++ i) f[x][i] = 1ll * i * a[x];
	for (int i = st[x]; ~i ;i = ed[i].next)
	{
		int rec = ed[i].to;
		if(rec == fa) continue;
		dfs(rec,x);
		for (int j = 1;j <= 20;++ j) pre[rec][j] = min(pre[rec][j - 1],f[rec][j]);
		for (int j = 20; j ;-- j) suf[rec][j] = min(suf[rec][j + 1],f[rec][j]);
		for (int j = 1;j <= 20;++ j) f[x][j] += min(pre[rec][j - 1],suf[rec][j + 1]);
	}
	return;
}

int main()
{
	scanf("%d",&T);
	while (T --)
	{
		scanf("%d",&n),cnt = 0,ans = Inf;
		for (int i = 1;i <= n;++ i)
		{
			scanf("%lld",&a[i]),st[i] = -1;
			for (int j = 0;j <= 21;++ j) f[i][j] = pre[i][j] = suf[i][j] = Inf;
		}
		for (int i = 1,u,v;i < n;++ i) scanf("%d%d",&u,&v),add(u,v),add(v,u);
		dfs(1,0);
		for (int i = 1;i <= 20;++ i) ans = min(ans,f[1][i]);
		printf("%lld\n",ans);
	}
	return 0;
}

E - Range Minimum Sum

        一道计数好题。

        题意:

        给一个长度为 n 的序列a,定义  f(a) = \sum_{l=1}^n\sum_{r=1}^n min_{l<=i<=i} (a_i) ,对于每个 i ,单独解决以下问题:

        从序列 a 中移除 a_i ,剩下的数记作序列 b ,计算 f(b) 。

        思路:

        我们首先计算一个数都不删减时的答案,再考虑删除每个数之后产生的影响。

        一个数都不删的情况:

        统计出每个位置左右第一个小于当前这个数的位置,分别记作 l_i 和 r_i ,那么这个位置上的贡献就是( i - l_i  )*  ( r_i - i ) * a_i,求 l,r 数组的过程可以用单调栈线性解决。

        删除位置i上的数对答案产生的影响:

        1. 首先显然要减去( i - l_i  )*  ( r_i - i ) * a_i

        2. 假设除去 i 之外的某个位置 j ,区间 [ l_i + 1 , r_i - 1 ] 覆盖了位置i,那么在删去 i 之后, j 的贡献也会减少。如果 i 位于左侧区间 [ l_i + 1 , j - 1 ],那么少了一个 i 对于 j 的贡献的影响就是减少 1 个 ( r_i - i ) * a_i 。i 位于右侧区间的情况同理。那么这段贡献的变化如何统计呢?我们可以枚举 j ,那么对于 [ l_i + 1 , j - 1 ] 和  [ j + 1 , r_i - 1 ] 区间内的每个 i ,删除 i 都会造成上述影响,这实际上是两段区间减,用差分数组维护即可。

        3. 对于某个位置 j ,若 l_j 或者 r_j 刚好等于 i,那么在删除i之后就会更新 j 的 l_j 或者 r_j ,更新之后 j 的贡献会增加。更新 l,r 的过程即在 i 的左右找出第一个比 a_j 小的数,这部分可以用二分 + ST表解决(如果用二分 + 线段树就会带两个 log ,可能超时)。

        时间复杂度:O(n log n)

        真是一道酣畅淋漓的计数好题!

#include<cstdio>
#include<cstring>
#include<stack>
using namespace std;

#define N 500005

int T,n,a[N],l[N],r[N],lg[N],f[N][22];
long long tot,ans[N],dif[N];

stack<int> st;

int min(int x,int y) { return x < y ? x : y ; }

void solveLR()
{
	while (!st.empty()) st.pop();
	for (int i = 1;i <= n + 1;++ i)
	{
		while (!st.empty() && a[i] < a[st.top()]) r[st.top()] = i,st.pop();
		st.push(i);
	}
	while (!st.empty()) st.pop();
	for (int i = n;i >= 0;-- i)
	{
		while (!st.empty() && a[i] < a[st.top()]) l[st.top()] = i,st.pop();
		st.push(i);
	}
	return;
}

void solveST()
{
	for (int j = 1;j <= lg[n + 1];++ j)
		for (int i = 0;i + (1 << j) - 1 <= n + 1;++ i)
			f[i][j] = min(f[i][j - 1],f[i + (1 << (j - 1))][j - 1]);
	return;
}

int findL(int x,int w)
{
	int ll = 0;
	int rr = x - 1;
	int tmp = 0;
	while (ll <= rr)
	{
		int mid = (ll + rr) >> 1;
		int len = x - mid;
		if(min(f[mid][lg[len]],f[x - (1 << lg[len])][lg[len]]) < w) tmp = mid,ll = mid + 1;
		else rr = mid - 1;
	}
	return tmp;
}

int findR(int x,int w)
{
	int ll = x + 1;
	int rr = n + 1;
	int tmp = n + 1;
	while (ll <= rr)
	{
		int mid = (ll + rr) >> 1;
		int len = mid - x;
		if(min(f[x + 1][lg[len]],f[mid - (1 << lg[len]) + 1][lg[len]]) < w) tmp = mid,rr = mid - 1;
		else ll = mid + 1;
	}
	return tmp;
}

int main()
{
	lg[1] = 0;
	for (int i = 2;i < N;++ i) lg[i] = lg[i >> 1] + 1;
	scanf("%d",&T);
	while (T --)
	{
		scanf("%d",&n),a[0] = a[n + 1] = 0,tot = dif[0] = 0ll;
		for (int i = 1;i <= n;++ i) scanf("%d",&a[i]),dif[i] = 0ll,f[i][0] = a[i];
		solveLR();
		solveST();
		for (int i = 1;i <= n;++ i) tot += 1ll * (i - l[i]) * (r[i] - i) * a[i];
		for (int i = 1;i <= n;++ i)
		{
			ans[i] = tot - 1ll * (i - l[i]) * (r[i] - i) * a[i];
			dif[l[i] + 1] -= 1ll * (r[i] - i) * a[i],dif[i] += 1ll * (r[i] - i) * a[i];
			dif[i + 1] -= 1ll * (i - l[i]) * a[i],dif[r[i]] += 1ll * (i - l[i]) * a[i];
		}
		for (int i = 1,nl,nr;i <= n;++ i)
		{
			nl = findL(l[i],a[i]);
			if(l[i]) ans[l[i]] += 1ll * (l[i] - nl - 1) * (r[i] - i) * a[i];
			nr = findR(r[i],a[i]);
			if(r[i] != n + 1) ans[r[i]] += 1ll * (nr - r[i] - 1) * (i - l[i]) * a[i];
		}
		for (int i = 1;i <= n;++ i)
		{
			dif[i] += dif[i - 1];
			ans[i] += dif[i];
			printf("%lld ",ans[i]);
		}
		printf("\n");
	}
	return 0;
}

总结

        事实摆在那,这场确实打得稀烂,最大的原因是 A 题滑铁卢,但本质上是思维上仍有缺口,不过好在是还有时间练习。D 题自己打的 dp 现在仍然不清楚错在哪里,可能是方法本身有点瑕疵,虽然 “ 到现在都不知道错哪了 ” 还是心里的一道坎,但多多学习正规的解法才是王道,很多时候得取其精华、去其糟粕。

  • 11
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值