第二十二届电子科技大学程序设计竞赛 UESTCPC 2024 题解 without A

第二十二届电子科技大学程序设计竞赛 - 决赛 题解

补题链接

ABCDEFGHIJK

D 黑白珠串

构造一个由 \(01\) 分别表示黑珠和白珠的字符串,给出 \(k\) 个条件,每个条件如下:

  • 给定 \(x,y\),要求黑白珠串中存在至少一个子串,满足子串的长度为 \(x\),且恰好包含 \(y\) 个黑珠。

请你为顾客构造出符合以上所有条件的黑白珠串,且满足珠串的长度最小。为了保证你构造的珠串满足上述条件,你还要对于每个条件,给出一个满足条件的子串的位置。

签到题,比较显然可以构造一段连续的 \(0\) 和一段连续的 \(1\) 组成的字符串。具体实现见代码。

#include <bits/stdc++.h>
 
const int K = 1e5 + 5;
 
int k;
int x[K], y[K];
 
int main() {
	scanf("%d", &k);
	for (int i = 1; i <= k; i ++)
		scanf("%d%d", &x[i], &y[i]);
	int l = 0, r = 0;
	for (int i = 1; i <= k; i ++) {
		l = std::max(l, x[i] - y[i]);
		r = std::max(r, y[i]);
	}
	printf("%d\n", l + r);
	for (int i = 1; i <= l; i ++)
		putchar('0');
	for (int i = 1; i <= r; i ++)
		putchar('1');
	printf("\n");
	for (int i = 1; i <= k; i ++)
		printf("%d\n", l - (x[i] - y[i]));
	return 0;
}

K 操作序列

给定一个长度为 \(n\) 的序列 \(a_1,a_2,\ldots,a_n\)

你需要选定一个整数 \(k\) \((1\leq k\leq n)\),然后对序列进行任意次操作,每次操作可以选择序列中 \(k\) 个不同的位置,使得这些位置的数乘上一个相同的非零整数,最终使得序列中所有的数相等。

输出满足条件的 \(k\) 的最大值。

\(k=1\) 的时候是可以使得序列中的所有数最终相等的,比如让最终每个数等于 \(\Pi a\)。我们思考 \(k=n-1\) 的时候,相当于给某个位置除上一个数,从感性角度理解似乎是对的。然后思考一下可以得出下面的构造方法。

执行 \(n\) 次操作,第 \(i\) 次操作给除第 \(i\) 个位置以外的数乘上 \(a_i\),最终的每个数也都等于 \(\Pi a\)

于是 \(k=n-1\) 是可行的,然后特判一下所有数相等时 \(k=n\) 的答案。

int n,a[N];
int main(){
	int T;scanf("%d",&T);
	while(T--){
		scanf("%d",&n);
		for(int i=1;i<=n;++i){
			scanf("%d",&a[i]);
		}
		bool flag=1;
		for(int i=2;i<=n;++i){
			if(a[i]!=a[1])flag=0;
		}
		if(flag)printf("%d\n",n);
		else printf("%d\n",n-1);
	}
	return 0;
}

赛时想唐了,这个 trick 还是比较经典的

I 取数游戏

有一个长度为 \(n\) 的序列,第 \(i\) 个数的权值为 \(a_i\)。两位选手 A 和 B 在做游戏,他们将对轮流对序列进行操作。每位选手每次有两种操作可以选择:

  • 删除序列中的任意两个的数 \(a_i,a_j\) \((i \neq j)\),并在序列中添加一个权值为 \(a_i+a_j\) 的数。当序列中只有一个数时,不能执行该操作。
  • 取走序列中一个数,将序列中剩下的数的权值全部累加给对方,结束游戏。

如果最后 A 得到的权值更多,则他胜利,否则 B 胜利。

A 先进行操作,问他是否有必胜策略。保证 \(a_i\) 之和为奇数。

首先如果原序列中就存在一个数大于其他数之和,则 A 先取,必胜。对于其他情况:

  • \(n=1\) 时,A 必胜。
  • \(n=2\) 时,一个数必然比另一个数大,A 直接取走,A 必胜。
  • \(n=3\) 时,A 不管怎么走都会 B 都会进入上面两个必胜态,故 A 必败。
  • \(n=4\) 时,A 如果选两个最小的数,这两个数组成的新数一定不会大于其他数之和,B 进入 \(n=3\) 的必败态,故 A 必胜。
  • \(n\geq 5\) 时,同理可以讨论,得到 \(n\) 为偶数时 A 必胜,反之 B 必胜。(注意每一步选数的过程中每个人都要保证操作结束后新数不能大于其他数之和)

然后赛时乱猜写了一个神秘的贪心模拟,本质是一回事,这里还是贴一下,懒得再写一遍更短的正解了。

int n;
ll sum=0,a[N];
multiset<ll>si;
int main(){
	int T;scanf("%d",&T);
	while(T--){
		si.clear();sum=0;
		scanf("%d",&n);
		for(int i=1;i<=n;++i){
			scanf("%lld",&a[i]);
			sum+=a[i];
			si.insert(a[i]);
		}
		ll lim=(sum/2)%2==0?sum/4:sum/2/2+1;
		bool flag=0;
		for(int t=1;;t^=1){
			// for(auto x:si){
			// 	printf("%lld ",x);
			// }
			// puts("");
			if(*si.rbegin()>sum/2){
				flag=t;
				break;
			}
			auto it=si.lower_bound(lim);
			if(it==si.end())--it;
			ll x=*it,y=0;++it;
			if(it!=si.end()){
				y=*it;
				if(x+y<=sum/2){
					si.erase(si.find(x));
					si.erase(si.find(y));
					si.insert(x+y);
					continue;
				}
			}
			--it;
			if(it==si.begin()){flag=t^1;break;}
			y=*--it;
			if(x+y<=sum/2){
				si.erase(si.find(x));
				si.erase(si.find(y));
				si.insert(x+y);
				continue;
			}
			if(it==si.begin()){flag=t^1;break;}
			x=*--it;
			si.erase(si.find(x));
			si.erase(si.find(y));
			si.insert(x+y);
		}
		if(flag)puts("YES");
		else puts("NO");
	}
	return 0;
}

C Add

给定一个长度为 \(n\) 的序列 \(a_1,a_2,\ldots,a_n\),初始时 \(a_i=i\)。对该序列进行 \(n-1\) 次操作,其中第 \(i\) 次操作为在 \([1,n-i]\) 中等概率选择一个整数 \(j\),令 \(a_j\) 变为 \(a_j+2a_{n-i+1}\)

求操作完毕后 \(a_1\) 的期望 \(\bmod\text{ }998244353\) 的值。

单独考虑序列中每个元素 \(a_i\) 经过 \((i-1)\) 操作之后对 \(a_1\) 的贡献。假设现在有值 \(x\) 加到了第 \(i\) 个位置上,最终他对 \(a_1\) 的贡献为 \(f(i)x\),官方题解称 \(f(i)\) 为从 \(i\) 开始往前加的期望系数。则可以得到递推关系

\[f(i)=\frac{2}{i-1}\sum_{j=1}^{i-1}f(j) \]

\(f(2)=2\),进而算出 \(f(3)=3\),利用数学归纳法,可以得出 \(f(i)=i\)

故最终的答案为

\[1+\sum_{i=2}^{n}f(i)a_i=\sum_{i=1}^{n}i^2=\frac{n(n+1)(2n+1)}{6} \]

注意需要取模

void work() {
	scanf("%lld", &n);
	printf("%lld\n", n * (n + 1) % mod * (2 * n + 1) % mod * qpow(6, mod - 2) % mod);
}
int main() {
	int T;
	scanf("%d", &T);
	while (T --)
		work();
	return 0;
}

事实证明顺着推是很难的,最终还是要观察+归纳法证明,赛时成功浪费 1 小时。

H 饮料

有一个果汁机,每分钟可以制作一杯任意体积的果汁。

\(n\) 个人排成一队。第 \(i\) 个人将在第 \(t_i\) 分钟走到果汁机前,并拿走当前已经制作的果汁中体积最大的一杯。第 \(i\) 个人拿到体积大于等于 \(a_i\) 的果汁就会满意。如果此时没有果汁,则第 \(i\) 个人也会不满意。

问是否能够让所有人满意。如果是,输出让所有人满意所需的果汁体积之和的最小值。

暂且只考虑一个时刻的最小果汁体积,还是令第 \(i\) 个人需要至少 \(a_i\) 的一杯果汁。则可以得出制作的第 \(i\) 个人实际拿的果汁体积应该为 \(\max_{j=i}^{n} a_i\),即这个人自身即之后所有果汁需求体积的最大值。

如果只有一个时刻,则之前这些果汁的制作顺序其实是无所谓的;但现在有多个时刻,我们需要考虑后面的果汁需求对前面的影响。于是我们考虑从后往前分配果汁,同时需要合理安排后面时刻的果汁制作顺序:越大的需求越应该放在后面处理。

于是我们维护一个 std::multiset 表示到当前人时之后需求的果汁体积,需要为这个人制作的果汁体积是 std::multiset 中元素中的最大值和这个人自身需求的最大值,然后在这个时刻和之前连续的一段没有需求的时刻分配尽可能大的需求。具体实现见代码。

int t[N],n,tim[N],tot;
ll a[N];
vector<int>lis[N];
map<int,ll>vis;
multiset<ll>si;
ll ans=0;
int main(){
	scanf("%d",&n);
	for(int i=1;i<=n;++i){
		scanf("%d",&t[i]);
		tim[++tot]=t[i];
	}
	tot=unique(tim+1,tim+1+tot)-tim-1;
	for(int i=1;i<=n;++i){
		t[i]=lower_bound(tim+1,tim+1+tot,t[i])-tim;
		lis[t[i]].push_back(i);
	}
	for(int i=1;i<=n;++i){
		scanf("%lld",&a[i]);
	}
	vector<ll>tmp;int loc=tim[tot];
	for(int i=tot;i>=1;--i){
		ll lim=0;
		if(si.size()){
			lim=*si.rbegin();
		}
		while(lis[i].size()){
			lim=max(lim,a[lis[i].back()]);
			si.insert(lim);
			lis[i].pop_back();
		}
		loc=min(loc,tim[i]);
		while(loc!=tim[i-1]&&!si.empty()){
			ll x=*si.rbegin();
			vis[loc--]=x;
			si.erase(si.find(x));
			ans+=x;
		}
	}
	if(loc<0||si.size())puts("-1");
	else printf("%lld\n",ans);
	return 0;
}

B 卡牌游戏

李华有一副卡牌,每张卡牌有颜色、数字以及价值三个属性。每张卡牌的颜色可以用一个 \([1,n]\) 中的整数代表,同样的,每张卡牌上会写有一个 \([1,n]\) 之间的整数。根据数字和颜色的不同,这些卡牌被分为 \(n^2\) 种。

为了方便表示,记一张颜色为 \(x\),数字为 \(y\) 的卡牌种类编号为 \((x-1)\times n+y\)。在牌堆中,第 \(i\) 种卡牌共有 \(a_i\) 张,每张卡牌的价值为 \(b_i\)

一开始,李华的手牌为空,初始积分为 \(0\),李华会进行 \(k\) 轮游戏。在每一轮的开始,李华会从牌堆里剩余的牌中随机取出一张牌,抽到每张牌的概率都是相等的。紧接着,如果手牌中含有与取出的牌数字颜色相同的牌,那么李华会将所有的符合上述条件的牌加上原本取出的牌放回牌堆,反之则将抽出的牌置于手牌中。

每一轮结束后,李华的积分会加上其手牌中所有牌的价值,请帮李华求出 \(k\) 轮后的期望总积分对 \(998244353\) 取模后的值。

比较板的矩阵快速幂维护 DP,这里不展开细节了。

由于拥有的手牌存在一定限制,手上拥有的手牌种类数实际上是比较少的,总共 \(208\) 种,我们用一个列向量维护每种状态某次操作后的概率,同时额外开一维维护某次操作之前的期望之和,则总的答案就是最后的列向量所有状态的概率乘价值之和加上额外那一位的期望之和(对应的是之前的时刻)。可以见官方题解,讲得很好。

const int MAXN = 1e5+5;
const int mod = 998244353;
int n, K, A[20], B[20], mp[1<<20];
int row[6], col[6], tot;
struct mat {
    int a[250][250], n, m;
    mat() { memset(a, 0, sizeof a); }
    mat operator * (const mat& t) const {
        mat r; r.n=n, r.m=t.m;
        for(int i=0; i<n; ++i)
            for(int j=0; j<m; ++j) if(a[i][j])
                for(int k=0; k<t.m; ++k)
                    r.a[i][k] = (r.a[i][k]+1ll*a[i][j]*t.a[j][k]%mod)%mod;
        return r;
    }
} t, ori, tt;
mat qkpow(int y) {
    mat ret; ret.n=ret.m=tot+1;
    for(int i=0; i<tot+1; ++i) 
        ret.a[i][i] = 1;
    while(y) {
        if(y&1) ret = ret*t;
        t = t*t; y >>= 1;
    } return ret;
}
int qkPow(int x, int y=mod-2) {
    int r = 1;
    while(y) {
        if(y&1) r=1ll*r*x%mod;
        x=1ll*x*x%mod; y>>=1;
    } return r;
}
bool check(int s) {
    for(int i=1; i<=n; ++i) 
        row[i] = col[i] = 0;
    for(int i=1; i<=n*n; ++i)
        if(s>>(i-1)&1) {
            int x = (i-1)/n+1;
            int y = (i-1)%n+1;
            if(row[x] || col[y]) return false;
            row[x] = col[y] = 1;
        }
    return true;
}

int trans(int s, int i) {
    int x = (i-1)/n+1;
    int y = (i-1)%n+1;
    int ret = s;
    for(int j=1; j<=n*n; ++j) 
        if(s>>(j-1)&1) {
            int xx = (j-1)/n+1;
            int yy = (j-1)%n+1;
            if(x==xx || y==yy) ret ^= (1<<(j-1));
        }
    if(ret==s) ret ^= (1<<(i-1));
    return ret;
}

signed main() {
    // freopen("r.in","r",stdin);
    n=read(9), K=read(9);
    ll suma = 0;
    for(int i=1; i<=n*n; ++i) 
        A[i]=read(9), suma += A[i];
    for(int i=1; i<=n*n; ++i) B[i]=read(9);
    if(suma == 0) return puts("0"), 0;
    for(int s=0; s<(1<<(n*n)); ++s) {
        if(! check(s)) continue;
        mp[s] = (++ tot);
        // printf("rela %d %d\n", s, mp[s]);
    }
    for(int s=0; s<(1<<(n*n)); ++s) {
        if(! mp[s]) continue;
        int cnt1 = 0;
        for(int i=1; i<=n*n; ++i)
            if(s>>(i-1)&1) ++ cnt1;
        ll mom = qkPow(suma-cnt1);
        for(int i=1; i<=n*n; ++i) {
            ll son = A[i];
            if(s>>(i-1)&1) -- son;
            if(son<0) continue;
            int state = trans(s, i);
            // printf("??? now %d->%d: %d\n", s, i, state);
            t.a[mp[s]-1][mp[state]-1] = (t.a[mp[s]-1][mp[state]-1]+son*mom%mod)%mod;
        }
    }
    for(int i=0; i<(1<<(n*n)); ++i) {
        if(! mp[i]) continue;
        ll val = 0;
        for(int j=1; j<=n*n; ++j)
            if(i>>(j-1)&1) val = (val+B[j])%mod;
        t.a[mp[i]-1][tot] = val;
    }
    t.a[tot][tot] = 1;
    t.n = t.m = ori.m = tot+1; ori.n = 1;
    ori.a[0][0] = 1;
    tt = t;
    t = qkpow(K);
    ori = ori*t;
    ll ans = ori.a[0][tot];
    for(int i=0; i<tot; ++i)
        ans = (ans+1ll*ori.a[0][i]*tt.a[i][tot]%mod)%mod;
    print(ans, '\n');
    return 0;
}

E 消消乐

bjh 有一个长度为 \(n\) 的只由字符 01 构成的字符串 \(s\)

bjh 的每次操作可以选择一个长度大于 1 的极大的只包含一种字符的子串,将这一整个子串修改为与其中字符相反的一个字符(如 \(11001\) 一次操作后可以改为 \(0001\)\(1111\))。

bjh 会一直操作下去,直到字符串无法再被操作,请帮他找到所有操作方案中最大操作次数和最小操作次数的差值。

我们无需关心长度大于 \(1\) 的极长同字符子串的具体长度,不妨将所有长度大于 \(1\) 的极长同字符子串合并为一个单元 \(\text{a}\),将长度为 \(1\) 的单个字符视作一个单元 \(\text{b}\),那么原题可以转化为

给一个只由 \(\text{ab}\) 构成的字符串,每次操作可以选择一个 \(\text{a}\) 字符,将他和他左右的字符合并为一个新的字符,这个字符为 \(\text{a}\),直到最后字符串只剩一个字符。

那么对于在两端的 \(\text{a}\) 字符,每次合并字符串总长度减小 \(1\);对于在中部的 \(\text{a}\) 字符,每次合并字符串长度减少 \(2\)。如果最大化操作次数,则优先对两端的 \(\text{a}\) 进行操作;最小化则对中部 \(\text{a}\) 进行操作。具体而言:

最大化操作时,找到离两端距离最近的 \(\text{a}\) 字符,对他一直进行操作,首先他会作为字符串的一端(在这个过程中每次长度 \(-2\)),最后他会在一端将字符串合并干净(在这个过程中每次长度 \(-1\))。

最小化操作稍微复杂,我们肯定是希望能有一个字符 \(\text{a}\) 出现在字符串中点,然后他就能一次性消干净。我们找到最接近中点的字符 \(\text{a}\),如果中点的另一侧有字符 \(\text{a}’\),则先对 \(\text{a}'\) 进行操作,使 \(\text{a}\) 成为中点,随后对 \(\text{a}\) 进行操作即可。

同时还可以这样理解,找到每个 \(\text{a}\) 字符进行长度 \(-2\) 操作可消除的部分(这部分的一端是原字符串的一端,然后 \(\text{a}\) 是这部分的中点),最后看看这些部分组合起来有哪些区域没消掉即可。本质上是同一回事。

具体实现见代码。

int n;
string s,t;

int main(){
	// std::cin.sync_with_stdio(0);
	// std::cin.tie(0);
	int Test;cin>>Test;
	while(Test--){
		cin>>n>>s;t.clear();
		int len=1;
		for(int i=1;i<n;++i){//题意转化
			if(s[i]!=s[i-1]){
				t.push_back(len==1?'b':'a');
				len=1;
			}else ++len;
		}
		t.push_back(len==1?'b':'a');
		n=(int)t.length();
		//cout<<t<<"\n";
		int minv=n+1,maxans=0,minans=0,num=0;
		for(int i=0;i<n;++i){
			if(t[i]=='a'){
				minv=min(minv,min(i,n-1-i));
			}//此时minv找到两端最近的距离
		}
		//cout<<minv<<"\n";
		if(minv==n+1){puts("0");continue;}
		maxans=minv+(n-2*minv-1);//minv次-2操作,(n-2*minv-1) 次-1操作

		vector<int>sum(n+1,0);//使用差分前缀和的形式看哪些位置不能被 a -2消除
		for(int i=0;i<n;++i){
			if(t[i]=='a'){
				if(i<=n-1-i){
					++sum[0],--sum[2*i+1];
				}else{
					++sum[2*i-n+1],--sum[n];
				}
			}
		}
		for(int i=0;i<n;++i){
			if(i)sum[i]+=sum[i-1];
			if(sum[i]==0)++num;
		}
		minans=num+(n-num)/2;
		printf("%d\n",maxans-minans);
	}
	return 0;
}

J 2-聚类算法

Alice 和 Bob 是两只 \(k\) 维空间中的生物。在他们生活的空间中,分布着 \(2n\) 个特征点,其中第 \(i\) 个特征点的坐标为 \((x_{i,1},x_{i,2},\ldots,x_{i,k})\)

在这个空间中,两点之间的距离被定义为它们之间的曼哈顿距离(即点 \(i\) 与点 \(j\) 之间的距离 \(\text{dist}_{i,j}=\sum_{p=1}^{k}|x_{i,p}-x_{j,p}|\))。

Alice 和 Bob 需要收集这些特征点。他们轮流从这 \(2n\) 个点中选走一个点,已经被选走的点不能被再次选走。Alice 先手。Alice 会将她选走的点放入集合 \(S_1\),而 Bob 会将他选走的点放入集合 \(S_2\)

设一个集合的价值 \(\text{val}(S)\) 为其中两两点之间的距离之和。Alice 希望最大化 \(\text{val}(S_1)-\text{val}(S_2)\),而 Bob 希望最小化 \(\text{val}(S_1)-\text{val}(S_2)\)

若 Alice 和 Bob 都采取最优策略,请你求出最终 \(\text{val}(S_1)-\text{val}(S_2)\) 的值会是多少?

\[\begin{aligned} & \sum_{i < j\in S_a} \text{dist}(p_i, p_j) - \sum_{i < j \in S_b} \text{dist}(p_i, p_j)\\ \\ =& \bigg(\sum_{i < j\in S_a} \text{dist}(p_i, p_j) + \sum_{i < j\in S_b} \text{dist}(p_i, p_j) + \sum_{i \in S_a, j\in S_b} \text{dist}(p_i, p_j) \bigg) - \bigg(\sum_{i < j \in S_b} \text{dist}(p_i, p_j) + \sum_{i > j\in S_b} \text{dist}(p_i, p_j) + \sum_{i \in S_a, j\in S_b} \text{dist}(p_i, p_j)\bigg)\\ \\ =& \sum_{i < j \in U} \text{dist}(p_i, p_j) - \sum_{i \in U,\ j \in S_b} \text{dist}(p_i, p_j) \end{aligned} \]

F 一站到底

给一棵树,每个树节点对应一道题目,完成每道题目的得分为 \(a_i\) 成功率为 \(p_i\),如果题目回答失败则立即结束答题。树以 \(1\) 为根,每道题目只有他的父亲题目完成后才能进行作答,编号为 \(1\) 的节点对应的题目可以直接作答。可以选择做题顺序,问最大得分期望。

如果没有树的限制,可以自由选择顺序进行,是经典的交换贪心问题,设第 \(x\) 个问题在第 \(y\) 个问题前面回答更优,定义 \(E_i=a_ip_i\) 则有

\[E_x+p_xE_y>E_y+p_yE_x\\ \frac{E_x}{1-p_x}>\frac{E_y}{1-p_y} \]

如果我们将“两个问题的前后顺序确定”称为合并,合并后成为“连通块”,于是我们每次找出 \(\frac{E}{1-p}\) 最大的连通块,那么他父亲(或对应的连通块)如果被选中回答了,则之后就应该最先回答这个连通块,就将他父亲和他合并。执行这样的合并知道最后只剩一个连通块,就是对应从节点 \(1\) 开始回答的答案。

G 打字

写了单题题解,可直接跳转

posted @ 2026-05-08 09:53  BigSmall_En  阅读(14)  评论(0)    收藏  举报