CSP-J2025

T1

题目

P14357 CSP-J 2025 拼数

思路

初步思路

把所有的数字拿出来,对数字排序。

代码实现

cpp 复制代码
#include<bits/stdc++.h>
using namespace std;
const int N=1e6+10;
string s;
int a[N];
int main()
{
	cin>>s;
	int len=s.size();
	int cnt=0;
	for(int i=0;i<len;i++)
	{
		if(s[i]>='0'&&s[i]<='9')
		{
			a[++cnt]=s[i]-'0';
		}
	}
	sort(a+1,a+cnt+1,greater<int>());
	for(int i=1;i<=cnt;i++)cout<<a[i];
	return 0;
 } 

从大到小sort(a+1,a+n+1,greater());

优化

比较简单的一道题,稳妥一点可以用计数排序。

计数排序的核心思想是:

统计每个值出现的次数

按顺序输出对应次数的值

因为只有10种数字('0'-'9'),数据范围极小,用计数排序的时间复杂度 O(n),比快速排序的 O(n log n) 更快。

计数排序的具体实现?

原数组: 4, 2, 2, 8, 3, 3, 1

  1. 统计每个数出现的次数
数值 1 2 3 4 5 6 7 8 9
次数 1 2 2 1 0 0 0 1 0
  1. 按从小到大输出
    1 2 2 3 3 4 8
计数排序代码
cpp 复制代码
#include<bits/stdc++.h>
using namespace std;
int a[1000];
string b;
int main(){
	cin>>b;
	for(int i=0;i<b.length();i++){
		a[b[i]]++;//统计每个数字的数量
	}
	for(char i='0';i<='9';i++){
		for(int j=1;j<=a[i];j++)cout<<i;
	}
	return 0;
}
本题目代码实现
cpp 复制代码
#include<bits/stdc++.h>
using namespace std;
int a[1000];
string b;
int main(){
	cin>>b;
	for(int i=0;i<b.length();i++){
		a[b[i]]++;//统计每个数字的数量
	}
	for(char i='9';i>='0';i--){
		for(int j=1;j<=a[i];j++)cout<<i;
	}
	return 0;
}

T2

P14358 CSP-J 2025 座位

题目含义

有一组数据,有一个n*m的矩阵

  • 把这组数据从小到大排序
  • 走蛇形放置在n*m的矩阵中
  • 求第一个人的排序后的行和列坐标

求解思路

初步思路

把这个矩阵构建出来。再寻找a1的位置。

代码实现

cpp 复制代码
#include<bits/stdc++.h>
using namespace std;
const int N=12;
int b[N*N];
struct Node{
	int x,y,val,id;
}a[N*N];
bool cmp(Node p,Node q)
{
	return p.val>q.val; 
}
int main()
{
	int n,m;
	cin>>n>>m;
	int len=n*m;
	for(int i=1;i<=len;i++)  
	{
		cin>>b[i];
		a[i]={0,0,b[i],i};
	}
	sort(a+1,a+len+1,cmp);
	int cnt=1;
	for(int i=1;i<=m;i++)//枚举每一列
	{
		if(i%2==1)//奇数 
		{
			for(int j=1;j<=n;j++)  //每一列要填n个数
			{
				a[cnt].x=i;
				a[cnt].y=j;
				cnt++;
			}
		 } 
		 else //偶数 
		 {
		 	for(int j=n;j>=1;j--)
		    {
		    	a[cnt].x=i;
				a[cnt].y=j;
				cnt++;
			}
		 }
	 } 
	 for(int i=1;i<=len;i++)
	 {
	 	if(a[i].id==1)
	 	{
	 		cout<<a[i].x<<" "<<a[i].y<<" ";
	 		break;
		 }
	 }
	return 0;
 } 

优化1

不用把每个人都填进去,只需要找a1的位置就好
cpp 复制代码
	R = a[1];
    sort(a + 1, a + n * m + 1, cmp);
    for (int i = 1; i <= n * m && a[i] ^ R; i++) {  //a[i] ^ R 等于 a[i] != R
        //y & 1 就是取 y 的二进制最低位,用于判断奇偶
        //奇数列,向下走,x增加,y不变 
        if (x < n && y & 1) { 
            x++;
            continue;
        }
        //最后一行,y+1,x不变 
        if (x == n && y & 1) {
            y++;
            continue;
        }
        //偶数列,从下往上,x--,y不变 
        if (x > 1 && !(y & 1)) {
            x--;
            continue;
        }
        //第一行, y+1,处理下一列 
        if (x == 1 && !(y & 1)) {
            y++;
            continue;
        }
    }

用二进制处理

  • ai ^ R ,值为1,即不同。
  • y&1判断是奇数列还是偶数列

优化2

能不能不模拟,利用数学方法直接计算答案呢?

第一步:先计算它的排序位置p

第二步:计算列

y=(p-1)/n+1

第三步:计算行

  • 如果是奇数列(从上往下填)
    x = p % n ? p % n : n
  • 如果是偶数列(从上往下填)
    x = n - (p % n ? p % n : n) + 1
cpp 复制代码
#include<bits/stdc++.h>
using namespace std;
int a[10009];
int main() {
    ios::sync_with_stdio(false);
    cin.tie(0);
    cout.tie(0);
    int n, m;
    cin >> n >> m;
    int len = n * m;

    for(int i = 1; i <= len; i++) {
        cin >> a[i];
    }
    
    // 第一步:计算 a[1] 的排名 p(从大到小)
    int p = 1; 
    for(int i = 2; i <= len; i++) {
        if(a[i] > a[1]) {
            p++;  
        }
    }
    
    // 第二步:计算列号 y
    int y = (p - 1) / n + 1;
    
    // 第三步:计算行号 x
    int x;
    if(y % 2 == 1) {  
        x = p % n ? p % n : n;
    } else {          
        x = n - (p % n ? p % n : n) + 1;
    }
    
    cout << y << " " << x << endl;
    
    return 0;
}

T3 CSP-J 2025 异或和

题目含义

求解思路一

1.利用前缀和思想,计算区间异或值

  • 异或和si的值计算
    si=si-1^ai
  • 区间异或值
    l,r之间的异或值为k
    sr^sl-1=k
    2.如何得到更多的不相交区间,贪心
  • 枚举右端点
  • 看之前是否有能与该右端点的si异或值为k的相匹配的左端点
    即是否有sr^k
    这里可以借助unordered_map实现
  • 如果有这样的左端点,再看与上一个区间是否重叠
  • 如果有这样的左端点,再看与上一个区间是否重叠
cpp 复制代码
#include<bits/stdc++.h>
using namespace std;
const int N=5e5+10;
unordered_map<int,int>mp;
int a[N],s[N];
int main()
{
	int n,k,cnt=0;
	cin>>n>>k;
	for(int i=1;i<=n;i++)
	{
		cin>>a[i];
		s[i]=s[i-1]^a[i];
	} 
	mp[0] = 0; 
	//枚举右端点
	int lat=0;
	for(int i=1;i<=n;i++)
	{
		if(mp.count(s[i]^k) && mp[s[i]^k]>=lat)
		{
			cnt++;
			lat=i;
		}
		mp[s[i]]=i;		
	 } 
	 cout<<cnt;
	return 0;
 } 

求解思路二

用dp求解

dp定义

dpi = 在前 i 个元素中,最多能选出多少个异或和为 k 的不相交区间。

转移方程

对于位置 i,我们考虑最后一个区间是否以 i 为右端点:

  • 不选i:dpi-1,不选以 i 结尾的区间
  • 选 i:
    有一个区间j,i,异或和为 si ^ sj-1 = k,然后加上前面部分dpj-1的最优解,即
cpp 复制代码
for (int i=1;i<=n;i++){
    for (int j=1;j<=i;j++) 
        dp[i]=max(dp[i], dp[j-1] + (int)((s[i]^s[j-1])==k));
}

但这个问题是时间复杂度O(n2)

怎么优化

第一个循环无法优化,对第二个循环想办法

这里就是找到一个区间的左端点,对于贪心而言,找到最靠右的左端点肯定是最好的。

结合上一方法,只要判断是否有si^k的左端点存在即可

那么

cpp 复制代码
#include <bits/stdc++.h>
using namespace std;
int a[500005];
int dp[500005];
int s[500005];
unordered_map<int,int> mp;
int main(){
    mp[0]=0;
    int n,k;
    cin>>n>>k;
    for (int i=1;i<=n;i++){
        cin>>a[i];
        s[i]=a[i]^s[i-1];
        
    }
    for (int i=1;i<=n;i++){
        //不选i
		dp[i]=dp[i-1];
		//选i 
        int cur=k^s[i];
        if (mp.count(cur)) dp[i]=max(dp[i],dp[mp[cur]]+1);
        mp[s[i]]=i;
    }
    cout<<dp[n];
}

我的bug

cpp 复制代码
int main(){
    mp[0]=0;
    int n,k;
    cin>>n>>k;
    for (int i=1;i<=n;i++){
        cin>>a[i];
        s[i]=a[i]^s[i-1];
        mp[s[i]]=i;
    }
    for (int i=1;i<=n;i++){
        //不选i
		dp[i]=dp[i-1];
		//选i 
        int cur=k^s[i];
        if (mp.count(cur)) dp[i]=max(dp[i],dp[mp[cur]]+1);
        
    }
    cout<<dp[n];
}

我是把mps\[i]=i;预处理出了,这样可能发生的问题是

mp 里存的是值为si所有位置,包括当前位置 i 和后面的值为si的位置。

要把mps\[i]=i;放在第二个循环中,边用变算,使得si对应的位置都是i之前的。

总结拓展

如果没有思路,看一下特殊性质

特殊性质A

特殊性质A ,所有的ai都是1,这里对应的k=0

那么当区间个数为偶数是,异或值为0,奇数为1.

所以

这是只针对性质A的代码

cpp 复制代码
#include <bits/stdc++.h>
using namespace std;

int main() {
    ios::sync_with_stdio(false);
    cin.tie(0);
    
    int n, k;
    cin >> n >> k;
    for (int i = 1; i <= n; i++) {
        int x;
        cin >> x;
    }
    
    if (k == 0) {
        cout << n / 2 << endl;
    } else if (k == 1) {
        cout << n << endl;
    } else {
        cout << 0 << endl;
    }
    
    return 0;
}

特殊性质B

特殊性质 B: 对于所有 1≤i≤n,均有 0≤ai≤1。

即每个数字是0或者1。

  • k=0的情况,即要求序列中1的数量都是偶数,遇到0就直接成组。

  • k=1的情况,即要求序列中1的数量都是奇数,即遇到1就是一个合法区间。

cpp 复制代码
#include<bits/stdc++.h>
using namespace std;
const int N=5e5+10; 
int a[N];
int main()
{
	int n,k;
	cin>>n>>k;
	int cnt=0,ans=0;
	for(int i=1;i<=n;i++) 
	{
		cin>>a[i];
		
		if(k==0)
		{
			if(a[i]==1) cnt++;
			if(cnt%2==0 || a[i]==0)
			{
				ans++;
				cnt=0;
			}
		//	cout<<"!"<<ans<<" "; 
		}
		else 
		{
			if(a[i]==1)  ans++;
		}
	 } 
	 cout<<ans; 
	 
	return 0;
 } 

以上的代码能将同时通过A、B的测试点。

T4 CSP-J 2025 多边形

题目分析

从 n 根木棍里选若干根,能拼成多边形的方案数。

  • 题目条件
    原条件:sum > 2 × max
    两边同减去max
    等价于:sum - max > max
    因为题目中说下标没有影响,只考虑集合。
    因此可以对所有木棍长度排序。
    对于第i根木棍,他前面的长度之和要大于当前木棍的长度。
排序的作用
因为题目只考虑集合(木棍的编号不影响),所以可以排序。
排序后,对于第 i 根木棍 ai,它前面的木棍(1 ~ i-1)长度都 ≤ ai,它后面的木棍(i+1 ~ n)长度都 ≥ ai
如果 ai 要成为最长木棍,那么:只能从它前面选木棍,后面的木棍不能选(否则最长木棍就不是 ai 了)。

题目转化为

对于第 i 根木棍,只能选它前面的木棍,前面的长度之和 > ai的方案数。

转化为01背包问题

01背包 本题
有 m 件物品 这里是前 i-1 根木棍
每件物品有重量 wj 木棍长度ai
每件物品只能选一次(选或不选) 木棍只看长度,用和没用
能凑出重量 ≤ W 的方案数 i之前选用木棍长度之和>ai的方案数

状态定义

dpi从前i-1根木棍中选择长度和>ai的方案数。

  • 计算i-1中选木棍的整体方案数,排除不合法的方案数,得到合法结果
    不合法的情况
不合法方案 数量
选0个 1
选1个 i-1
选2个及以上但和 ≤ ai 用dp计算

dpi表示在处理到第 i 根木棍时,它前面所有木棍中,能够凑出长度和 ≤ ai 的方案总数。

  1. 进一步细化就是,dpi的计算为在前i-1根木棍中,
    前i-1根木棍中选使其长度为0的方案+前i-1根木棍中选使其长度为1的方案+前i-1根木棍中选使其长度为2的方案+......+前i-1根木棍中选使其长度为ai的方案
    以上这些都是不合法的,需要减去,用代码实现就是
cpp 复制代码
for(int i=1;i<=n;i++)
	{
		long long sum=0;
		//因为这里计算的是不合法的, 不合法即长度之和<=a[i]
		//要计算出长度[0,a[i]]这个范围内的每一种方案数 
		for(int j=0;j<=a[i];j++)
		{
			 sum=(sum+dp[j])%mod;
		}
		ans=(ans-sum+mod)%mod;
	 } 
  1. dpj的值是多少呢?
    长度为j的方案数dpj,细分为用不用ai
用不用ai dp\[\] 解释
不用ai dpj 从前面 i-1 根中凑出长度 j 的方案
用ai dpj-a\[i] 前面凑出 j-ai,加上 ai 后总长度为 j

回顾01背包

因为每根木棍只有两种状态:

选:从 j-ai 转移过来

不选:保持原来的 dpj

01 背包的"选/不选"模型。

cpp 复制代码
for(int i=1;i<=n;i++)
	{
		long long sum=0;
		//因为这里计算的是不合法的, 不合法即长度之和<=a[i]
		//要计算出长度[0,a[i]]这个范围内的每一种方案数 
		for(int j=0;j<=a[i];j++)
		{
			sum=(sum+dp[j])%mod;
		}
		ans=(ans-sum+mod)%mod;
		// //前i-1个木棍中选长度为j  和 长度为j-a[i]配合a[i]长度恰好为j的两种方案
		for(int j=5000;j>=a[i];j--)
		{
			dp[j]=(dp[j]+dp[j-a[i]])%mod;
		}
	 } 

重点:为何逆序

保证每个木棍只被用一次

dp 中已经包含了所有单根木棍的方案!不用再减去

代码实现

cpp 复制代码
#include<bits/stdc++.h>
using namespace std;
const int mod=998244353;
int a[5005],dp[5005]; 
long long ans;
long long p[5005];
int main()
{
	int n;
	cin>>n;
	for(int i=1;i<=n;i++)  cin>>a[i];
	sort(a+1,a+n+1);
	
p[0]=1;
	for(int i=1;i<=5000;i++)
	{
		p[i]=p[i-1]*2%mod;
	}	
	ans=(p[n]-1+mod)%mod; 
	
	dp[0]=1;
	for(int i=1;i<=n;i++)
	{
		long long sum=0;
		//因为这里计算的是不合法的, 不合法即长度之和<=a[i]
		//要计算出长度[0,a[i]]这个范围内的每一种方案数 
		for(int j=0;j<=a[i];j++)
		{
			sum=(sum+dp[j])%mod;
		}
		ans=(ans-sum+mod)%mod;
		// //前i-1个木棍中选长度为j  和 长度为j-a[i]配合a[i]长度恰好为j的两种方案
		for(int j=5000;j>=a[i];j--)
		{
			dp[j]=(dp[j]+dp[j-a[i]])%mod;
		}
	 } 
	 cout<<ans;
	return 0;
 }