牛客周赛Round149

今天状态一般,A了四个题

看题吧

目录

A:小红的电话亭

签到题

B:小红的01串构造

字符串构造

C:小红的矩阵构造

还是构造

[1. 最小合法总和推导](#1. 最小合法总和推导)

[2. 存在性判断](#2. 存在性判断)

[3. 增量构造方法](#3. 增量构造方法)

D:小红的数字染色

贪心+前缀和+双指针

核心数学结论

代码实现思路

E:小红的矩阵计数

状态压缩+线性DP

核心转化:把二维约束降为相邻列约束

状态设计

状态转移规则

逐列可行状态判断


A:小红的电话亭

签到题

cpp 复制代码
#include<bits/stdc++.h>
using namespace std;
string s;
int main()
{
	cin>>s;
	for(int i=0;i<s.size();i++)
	{
		cout<<s[i];
		if(i==2||i==6)cout<<'-';
	}
	return 0;
}

B:小红的01串构造

字符串构造

先判断n的奇偶性,类似于111111000000的肯定是最好的呀

cpp 复制代码
#include<bits/stdc++.h>
using namespace std;
int n;
int main()
{
	cin>>n;
	if(n&1)
	{
		cout<<-1<<endl;
		return 0;
	}
	for(int i=1;i<=n/2;i++)cout<<1;;
	for(int i=n/2+1;i<=n;i++)cout<<0;
	return 0;
}

C:小红的矩阵构造

还是构造

整体采用「最小基底 + 增量集中填充」的构造策略,分为三步:先算出合法矩阵的最小总和,判断是否有解;有解则先铺最小基底,再把多余的增量集中加到一个格子上。

1. 最小合法总和推导

要满足「相邻不等、全正整数」的约束,总和最小的构造方式就是国际象棋棋盘式填充:

  • 奇数行奇数列、偶数行偶数列填 1
  • 其余位置填 2

这种布局天然保证相邻格子一个是 1、一个是 2,必然不相等,且使用的都是最小的正整数,因此总和是所有合法矩阵里最小的。

设总格子数 tot = n * m:

  • 值为 2 的格子数量:tot / 2(整数除法,向下取整)
  • 值为 1 的格子数量:tot - tot / 2

因此最小总和为: min_sum = 1 * (tot - tot/2) + 2 * (tot/2) = tot + tot / 2

2. 存在性判断

  • 若 k < min_sum:不可能构造出合法矩阵,直接输出 NO。 原因:所有元素至少为 1,且相邻不能相等,不可能全填 1,最小总和就是棋盘布局的和,比这个更小的话无解。
  • 若 k >= min_sum:一定存在解,接下来进行构造。

3. 增量构造方法

计算差值 sup = k - min_sum,这是我们需要额外增加的数值。

核心技巧:把所有增量全部加到同一个值为 2 的格子上。 选择值为 2 的格子的原因:它的上下左右四个相邻格子全都是 1,哪怕把它的值改成 2 + sup(一个很大的数),它和周围的 1 依然永远不相等,完全不会破坏「相邻不等」的规则。

修改后矩阵总和恰好为 min_sum + sup = k,且依然满足好矩阵的全部条件。

cpp 复制代码
#include<bits/stdc++.h>
using namespace std;
int n,m,k;
typedef long long LL;
int main()
{
	cin>>n>>m>>k;
	LL sum=1LL*n*m;
	sum+=sum/2;
	if(k<sum)
	{
		cout<<"NO"<<endl;
		return 0;
	}
	cout<<"YES"<<endl;

	LL sup=k-sum;
	//cout<<sup<<endl;
	bool flag=false;
	for(int i=1;i<=n;i++)
	{
		for(int j=1;j<=m;j++)
		{
			int x=2;
			if(i%2==1&&j%2==1||i%2==0&&j%2==0)x=1;
			if(x==2&&!flag)
			{
				cout<<x+sup<<' ';
				flag=true;
			}
			else cout<<x<<' ';
		}
		cout<<endl;
	}
	return 0;
}

D:小红的数字染色

贪心+前缀和+双指针

核心数学结论

设数组总和为 sum,选出的连续子数组和为 x,未被选中的元素和就是 sum - x。 我们的目标是最大化 x * (sum - x)。这是一个开口向下的二次函数,对称轴在 x = sum / 2 处: x 越接近 sum 的一半,乘积就越大。

因此问题转化为:在所有连续子数组的和中,找到最接近 sum/2 的那个值。

代码实现思路

  1. 从中点左侧找:找所有 不超过 ceil(sum/2)(上取整的一半)的连续子数组和,取其中最大的,也就是离中点最近的左侧点。
  2. 从中点右侧找:找所有 不小于 floor(sum/2)(下取整的一半)的连续子数组和,取其中最小的,也就是离中点最近的右侧点。
  3. 比较两个候选谁离中点更近,用更优的那个计算最终乘积。

因为数组全是正整数,前缀和严格单调递增,所以两次查找都可以用 ** 双指针(滑动窗口)** 在线性时间内完成,总时间复杂度 O (n)。

cpp 复制代码
#include<bits/stdc++.h>
using namespace std;
typedef long long LL;
const int N=2e5+10;
const int mod=998244353;
LL a[N];
int main()
{
	int n;
	cin>>n;
	LL sum=0;
	for(int i=1;i<=n;i++)
	{
		cin>>a[i];
		sum+=a[i];
	}
	LL res1=1e18;
	LL res2=1e18;
	LL num1=sum/2+sum%2;
	for(int i=1;i<=n;i++)a[i]+=a[i-1];
	int j=1;
	for(int i=1;i<=n;i++)
	{
		
		while(a[i]-a[j-1]>num1)j++;
		res1=min(res1,num1-(a[i]-a[j-1]));
	}
	j=1;
	LL num2=sum/2;
	for(int i=1;i<=n;i++)
	{
		while(j <= n && a[j]-a[i-1]<num2)
		{
			j++;
		}
        if(j <= n)
        {
		    res2=min(res2,a[j]-a[i-1]-num2);
        }
		if(j > n)break;
	}
	if(res1<=res2)
    {
        LL x = (num1 - res1) % mod;
        LL y = (sum - num1 + res1) % mod;
        cout << x * y % mod << endl;
    }
	else
    {
        LL x = (num2 + res2) % mod;
        LL y = (sum - num2 - res2) % mod;
        cout << x * y % mod << endl;
    }
	return 0;
	
}

E:小红的矩阵计数

状态压缩+线性DP

核心转化:把二维约束降为相邻列约束

矩阵只有固定 2 行,任意一个 2×2 的区域,本质就是相邻的两列拼接而成。 四个元素全相同的情况只有两种:

  1. 相邻两列都是「上 0、下 0」→ 拼出四个 0,非法
  2. 相邻两列都是「上 1、下 1」→ 拼出四个 1,非法

除此之外所有相邻列的组合都是合法的。比如两列都是 0/1,拼出来是 0 0 / 1 1,四个元素不全相同,完全合法。

于是问题就转化为: 给每一列填 0/1(受原字符限制),要求不能出现连续两列全 0、也不能出现连续两列全 1,求总方案数。 这是非常经典的线性动态规划问题。

状态设计

每一列只有上下两个格子,每个格子是 0 或 1,总共只有 4 种固定形态,我们给它们编号:

  • 状态 0:上行 0,下行 0 → 全 0 列
  • 状态 1:上行 0,下行 1 → 混合列
  • 状态 2:上行 1,下行 0 → 混合列
  • 状态 3:上行 1,下行 1 → 全 1 列

定义 dp is 表示:处理完前 i 列,且第 i 列恰好是状态 s 时,合法的填法总数。 最终答案就是第 n 列四个状态的方案数之和。

状态转移规则

我们先算出前一列所有状态的方案总和 sum_prev,再根据当前列的状态做调整:

  1. 当前列是状态 0(全 0 列) 前一列不能是状态 0(否则连续两列全 0,非法),因此转移值 = sum_prev - dp i-10
  2. 当前列是状态 3(全 1 列) 前一列不能是状态 3,因此转移值 = sum_prev - dp i-13
  3. 当前列是状态 1 或 状态 2(混合列) 混合列和任何列相邻都不会出现四个全相同,因此前一列可以是任意状态,转移值 = sum_prev

注意:取模运算中减法可能得到负数,所以要先加模数再取模,保证结果非负

逐列可行状态判断

对于第 i 列的两个字符(上行、下行),要先判断每个状态是否能填出来:

  • 字符是 '0' → 对应位置只能填 0
  • 字符是 '1' → 对应位置只能填 1
  • 字符是 '?' → 0 和 1 都可以

上下两个位置都匹配,这个状态就是可行的,方案数为 1;否则为 0。

cpp 复制代码
#include<bits/stdc++.h>
using namespace std;
typedef long long LL;
const int N = 2e5 + 10;
const int mod = 998244353;
LL dp[N][4];
int main()
{
    int n;
    string s1, s2;
    cin >> n >> s1 >> s2;
    char c1 = s1[0], c2 = s2[0];
    dp[1][0] = ((c1 == '0' || c1 == '?') && (c2 == '0' || c2 == '?')) ? 1 : 0; // 0 0
    dp[1][1] = ((c1 == '0' || c1 == '?') && (c2 == '1' || c2 == '?')) ? 1 : 0; // 0 1
    dp[1][2] = ((c1 == '1' || c1 == '?') && (c2 == '0' || c2 == '?')) ? 1 : 0; // 1 0
    dp[1][3] = ((c1 == '1' || c1 == '?') && (c2 == '1' || c2 == '?')) ? 1 : 0; // 1 1
    for(int i = 2; i <= n; i++)
    {
        c1 = s1[i-1];
        c2 = s2[i-1];
        LL sum_prev = (dp[i-1][0] + dp[i-1][1] + dp[i-1][2] + dp[i-1][3]) % mod;
        if((c1 == '0' || c1 == '?') && (c2 == '0' || c2 == '?'))
            dp[i][0] = (sum_prev - dp[i-1][0] + mod) % mod;
        else
            dp[i][0] = 0;
        if((c1 == '0' || c1 == '?') && (c2 == '1' || c2 == '?'))
            dp[i][1] = sum_prev % mod;
        else
            dp[i][1] = 0;
        if((c1 == '1' || c1 == '?') && (c2 == '0' || c2 == '?'))
            dp[i][2] = sum_prev % mod;
        else
            dp[i][2] = 0;
        if((c1 == '1' || c1 == '?') && (c2 == '1' || c2 == '?'))
            dp[i][3] = (sum_prev - dp[i-1][3] + mod) % mod;
        else
            dp[i][3] = 0;
    }
    LL ans = (dp[n][0] + dp[n][1] + dp[n][2] + dp[n][3]) % mod;
    cout << ans << endl;

    return 0;
}