CSP2023-S2题解汇总
xinxijishu110 · · 个人记录
第一题
[CSP-S 2023] 密码锁【民间数据】
题目描述
小 Y 有一把五个拨圈的密码锁。如图所示,每个拨圈上是从
因为校园里比较安全,小 Y 采用的锁车方式是:从正确密码开始,随机转动密码锁仅一次;每次都是以某个幅度仅转动一个拨圈或者同时转动两个相邻的拨圈。
当小 Y 选择同时转动两个相邻拨圈时,两个拨圈转动的幅度相同,即小 Y 可以将密码锁从
时间久了,小 Y 也担心这么锁车的安全性,所以小 Y 记下了自己锁车后密码锁的
为了检验这么锁车的安全性,小 Y 有多少种可能的正确密码,使得每个正确密码都能够按照他所采用的锁车方式产生锁车后密码锁的全部
输入格式
输入的第一行包含一个正整数
接下来
输出格式
输出一行包含一个整数,表示密码锁的这
样例 #1
样例输入 #1
1
0 0 1 1 5
样例输出 #1
81
提示
【样例 1 解释】
一共有
其中转动一个拨圈的方案有
【样例 2】
见选手目录下的 lock/lock2.in 与 lock/lock2.ans。
【数据范围】
对于所有测试数据有:
| 测试点 | 特殊性质 | |
|---|---|---|
| 无 | ||
| 无 | ||
| A | ||
| 无 |
特殊性质 A:保证所有正确密码都可以通过仅转动一个拨圈得到测试数据给出的
题解
首先可以将所有的00000~99999的字符串映射(map<string,int>)为编号0~99999,,也可以直接i
然后用 桶 统计编号出现的次数;
如果枚举每个正确答案,尝试按要求旋转45+36次,看看能否推出所有的n个错误答案,复杂度为100000
因此可以逆向思维,由于n个答案是错误的,那么从每个错误答案可以逆推45+36=81次,每个错误答案最多可以推出81个新串(有些串 不新 ,是因为可能由错误答案串推出其他错误答案串,这些串需要排除),这些新串都可能是正确答案,最后遍历所有的100000个桶,如果当前编号桶的数值为n,则说明这个串可以由所有n个错误答案推出,累积给ans即可,时间复杂度骤降至
100000
代码一:map<string,int>mp;
建立串和编码的映射,编码放桶中
#include<bits/stdc++.h>
using namespace std;
#define N 200010
map<string,int>mp;
int tong[N];
string tt[N];
int n,ans;
int main()
{
//freopen("lock.in","r",stdin);
//freopen("lock.out","w",stdout);
int cnt=1;
cin>>n;
if(n==1)
{
puts("81");
return 0;
}
for(int i=0;i<=9;++i)
for(int j=0;j<=9;++j)
for(int k=0;k<=9;++k)
for(int l=0;l<=9;++l)
for(int m=0;m<=9;++m)
{
string st;
st+=(char)(i+'0');
st+=(char)(j+'0');
st+=(char)(k+'0');
st+=(char)(l+'0');
st+=(char)(m+'0');
mp[st]=cnt;
cnt++;
//cout<<st<<endl;
}
int x;
for(int i=1;i<=n;++i)
{
for(int j=1;j<=5;++j)
{
scanf("%d",&x);
tt[i]+=(char)(x+'0');
}
tong[mp[tt[i]]]=-1;
}
for(int i=1;i<=n;++i)
{
for(int j=0;j<=4;++j) //tt[i]
{
for(int k=1;k<=9;++k)
{
string st=tt[i];
//cout<<"处理前:"<<st<<endl;
st[j]=(char)((st[j]-'0'+k)%10+'0');
if(tong[mp[st]]==-1)continue;
//cout<<st<<endl;
tong[mp[st]]++;
}
}
for(int j=0;j<=3;++j)
{
for(int k=1;k<=9;++k)
{
string st=tt[i];
st[j]=(char)((st[j]-'0'+k)%10+'0');
st[j+1]=(char)((st[j+1]-'0'+k)%10+'0');
if(tong[mp[st]]==-1)continue;
//cout<<st<<endl;
tong[mp[st]]++;
}
}
}
for(int i=1;i<=cnt;++i)
if(tong[i]==n)
ans++;
cout<<ans<<endl;
return 0;
}
代码二:直接处理每个串形成的数值
#include<bits/stdc++.h>
using namespace std;
#define N 200010
int tong[N];
int a[10][6],b[10];
int n,ans;
int main()
{
//freopen("lock.in","r",stdin);
//freopen("lock.out","w",stdout);
cin>>n;
if(n==1)
{
puts("81");
return 0;
}
int x;
for(int i=1;i<=n;++i)
{
int st=0;
for(int j=1;j<=5;++j)
{
st*=10;
scanf("%d",&x);
a[i][j]=x;
st+=x;
}
b[i]=st;
tong[st]=-1;
}
for(int i=1;i<=n;++i)
{
for(int j=1;j<=5;++j)
{
int p=(int)pow(10,5-j);
for(int k=1;k<=9;++k)
{
int st=b[i];
st-=a[i][j]*p; //当前位清0再转
int xx=(a[i][j]+k)%10;
st+=xx*p;
if(tong[st]!=-1)
tong[st]++;
}
}
for(int j=1;j<=4;++j)
{
int p=(int)pow(10,5-j);
int q=(int)pow(10,5-j-1);
for(int k=1;k<=9;++k)
{
int st=b[i];
st-=a[i][j]*p; //当前位清0再转
st-=a[i][j+1]*q;
int xx=(a[i][j]+k)%10;
int yy=(a[i][j+1]+k)%10;
st+=xx*p+yy*q;
if(tong[st]!=-1)
tong[st]++;
}
}
}
for(int i=1;i<=100000;++i)
if(tong[i]==n)
ans++;
cout<<ans<<endl;
return 0;
}
第二题
[CSP-S 2023] 消消乐【民间数据】
题目背景
本题提供两个版本的民间数据,供参考。
题目描述
小 L 现在在玩一个低配版本的消消乐,该版本的游戏是一维的,一次也只能消除两 个相邻的元素。
现在,他有一个长度为
其中每次操作可以从字符串中删除两个相邻的相同字符,操作后剩余字符串会拼接在一起。
小 L 想知道,这个字符串的所有非空连续子串中,有多少个是可消除的。
输入格式
输入的第一行包含一个正整数
输入的第二行包含一个长度为
输出格式
输出一行包含一个整数,表示题目询问的答案。
样例 #1
样例输入 #1
8
accabccb
样例输出 #1
5
提示
【样例 1 解释】
一共有 cc、acca、cc、bccb、accabccb。
【样例 2】
见选手目录下的 game/game2.in 与 game/game2.ans。
【样例 3】
见选手目录下的 game/game3.in 与 game/game3.ans。
【样例 4】
见选手目录下的 game/game4.in 与 game/game4.ans。
【数据范围】
对于所有测试数据有:
| 测试点 | 特殊性质 | |
|---|---|---|
| 无 | ||
| 无 | ||
| 无 | ||
| A | ||
| B | ||
| 无 | ||
| 无 |
特殊性质 A:字符串中的每个字符独立等概率地从字符集中选择。
特殊性质 B:字符串仅由 a 和 b 构成。
题解
第一种方法:暴力
考虑枚举每个点为起点,之后连续的可消除串有多少,可过8000
#include<bits/stdc++.h>
using namespace std;
int n,cnt,pre,ans;
string st;
int main()
{
//freopen("game.in","r",stdin);
//freopen("game.out","w",stdout);
cin>>n>>st;
for(int i=0;i<n;++i)
{
pre=0;
stack<char>zhan;
for(int j=i;j<n;++j)
{
zhan.push(st[j]);
while(zhan.size()&&st[j+1]==zhan.top()) //while非常精妙,感谢蔡睿
{
zhan.pop();
j++;
}
if(zhan.size()==0) //这里只要栈空即累加答案
pre++;
}
ans+=pre;
}
cout<<ans<<endl;
return 0;
}
正解1:正向DP
知乎题解1
#include <bits/stdc++.h>
using namespace std;
using LL = long long;
const int N = 2e6 + 5;
char a[N];
int f[N], g[N][26];
int main() {
int n;
scanf("%d%s", &n, a + 1);
LL ans = 0;
for (int i = 1; i <= n; i++) {
int x = a[i] - 'a';
int k = g[i - 1][x] - 1; // 消掉最近的一段后,上一段的结尾是k
if (k >= 0) {
ans += f[i] = f[k] + 1;
for (int j = 0; j < 26; j++) g[i][j] = g[k][j];
}
g[i][x] = i;
}
printf("%lld\n", ans);
return 0;
}
正解2:逆向DP
题解2
#include<bits/stdc++.h>
using namespace std;
#define int long long
/*
括号匹配的dp思想:
nxt[i]状态:表示存放下一个匹配的字符的位置
dp[i]状态:表示以i作为开头的匹配序列的数量
状态转移方程:dp[i]=dp[nxt[i]+1]+1
时间复杂度-O(n)-100pts
*/
const int N = 2e6 + 10;
int n;
string str;
int nxt[N], dp[N];
map<int, int> mp[N];//mp数组,用于存放每个位置之后的字符及其对应的位置
signed main() {
cin >> n >> str;
str = ' ' + str;
//初始化dp和nxt数组
for (int i = 1; i <= n + 1; i++) dp[i] = 0, nxt[i] = -1;
//倒着遍历字符序列
for (int i = n; i >= 1; i--) {
//在i+1位置之后查找是否存在与当前i位置相同的字符
if (mp[i + 1].count(str[i])) {
int pos = mp[i + 1][str[i]];//找到与当前i位置相同的字符位置pos
nxt[i] = pos;//记录匹配位置
swap(mp[i], mp[pos + 1]);//更新mp
if (pos < n) mp[i][str[pos + 1]] = pos + 1;//更新mp
}
mp[i][str[i]] = i;//更新mp
}
int ans = 0;
for (int i = n; i >= 1; i--) {
if (nxt[i] == -1) continue;
dp[i] = dp[nxt[i] + 1] + 1;//计算dp[i]
ans += dp[i];
}
cout << ans << endl;
return 0;
}
正解3:Trie树(字典树)
#include <bits/stdc++.h>
using namespace std;
using LL = long long;
const LL mod = 1e9 + 7;
const int N = 2e6 + 5;
char a[N]; //字符串序列
char s[N]; //模拟栈
int trie[N][26], fa[N], val[N], ck = 1;
//ch[N][26]是建立字典树,fa[N]用来继承前序,val[N]表示当前可以继承的可消除串数量
int main() {
int n;
scanf("%d%s", &n, a + 1);
LL ans = 0;
int tot = 0, u = ck;
val[u]++;
for (int i = 1; i <= n; i++) {
//printf("正在处理第%d个字符,当前字符为:%c\n",i,a[i]);
if (s[tot] == a[i]) {
tot--;
//printf("当前%c和栈顶相同,消除栈顶,%d上跳到fa[%d]=",a[i],u,u);
u = fa[u]; //u上跳到 fa[u]
//printf("%d\n",u);
}
else {
s[++tot] = a[i];
//printf("%c入栈,",a[i]);
int &v = trie[u][a[i] - 'a'];
//printf("当前v=trie[%d][%d]=%d\n",u,a[i]-'a',v);
//int &v这里是“引用”的用法 ,对v的处理会同步处理给 ch[u][a[i] - '0'];
if (v == 0)
{
//printf("因为trie[%d][%d]==0,因此建立新节点;\n",u,a[i]-'a');
v = ++ck, fa[v] = u; //建立trie树新节点和继承关系
//printf("令trie[%d][%d]=%d,fa[%d]=%d;\n",u,a[i]-'a',ck,v,u);
}
//printf("u=%d下跳到",u);
u = v; //u下跳到 v
//printf("v=%d;\n",u);
}
ans += val[u]; //每次到 u,都需要更新 u累积的可消除子串数量
//printf("val[%d]=%d,ans=ans+val[%d]=%d,然后val[%d]++,变为了",u,val[u],u,ans,u);
val[u]++; //每次回到 u,正明有一个新的可消除串回到了 u,val[u]++;
//printf("%d\n",val[u]);
//puts("");
}
printf("%lld\n", ans);
return 0;
}
第三题需要类,暂时跳过
第四题
[CSP-S 2023] 种树【民间数据】
题目描述
你是一个森林养护员,有一天,你接到了一个任务:在一片森林内的地块上种树,并养护至树木长到指定的高度。
森林的地图有
你的目标是:在每片地块上均种植一棵树木,并使得
你每天可以选择一个未种树且与某个已种树的地块直接邻接(即通过单条道路相连)的地块,种一棵高度为
对每个地块而言,从该地块被种下树的当天开始,该地块上的树每天都会生长一定的高度。由于气候和土壤条件不同,在第
你想知道:最少需要多少天能够完成你的任务?
输入格式
输入的第一行包含一个正整数
接下来
接下来
输出格式
输出一行仅包含一个正整数,表示完成任务所需的最少天数。
样例 #1
样例输入 #1
4
12 1 1
2 4 -1
10 3 0
7 10 -2
1 2
1 3
3 4
样例输出 #1
5
提示
【样例 1 解释】
第
第
第
第
第
【样例 2】
见选手目录下的 tree/tree2.in 与 tree/tree2.ans。
【样例 3】
见选手目录下的 tree/tree3.in 与 tree/tree3.ans。
【样例 4】
见选手目录下的 tree/tree4.in 与 tree/tree4.ans。
【数据范围】
对于所有测试数据有:
特殊性质 A:对于所有
特殊性质 B:对于所有
特殊性质 C:与任何地块直接相连的道路均不超过
特殊性质 D:对于所有
性质A:
每天增长量必然大于1,a[i]/b[i]算出每个位置需要的成长天数,可以暴力贪心,枚举已经种树的点,找到与这些点连接的 尚未种树的儿子中 的最大需要天数的儿子去种(最大需要天数的坑一定先种,这样他成长的天数可以覆盖掉其他树一部分成长天数成本,一定最优,种树后将他所有的儿子加入优先队列,优先队列中的点就是下一天可以种的坑)。
开个优先队列(大根堆),其中存放着准备种树的点(开始只有1号坑),每次取出堆顶(要种的坑),然后将他所有儿子加入堆(以需要的成长天数排序),种的次序(计数)就是他开始的天数,加上他的需要成长天数,所有坑中的最大值就是答案。时间复杂度O(n)获得高贵的15分。
#include<bits/stdc++.h>
using namespace std;
#define int long long
#define N 100010
int n;
int a[N],b[N],c[N],vis[N],dp[N],cnt=0,ans=0;
priority_queue<pair<int,int> >que;
vector<int>vec[N];
signed main()
{
//freopen("tree.in","r",stdin);
//freopen("tree.out","w",stdout);
cin>>n;
int flag=0;
for(int i=1;i<=n;++i)
{
scanf("%lld%lld%lld",&a[i],&b[i],&c[i]);
if(c[i]!=0)
flag=1; //判断是否为性质A
}
int x,y;
for(int i=1;i<n;++i)
{
scanf("%lld%lld",&x,&y);
vec[x].push_back(y);
vec[y].push_back(x);
}
if(flag==0)
{
que.push(make_pair(ceil(a[1]/b[1]),1));
while(que.size())
{
int u=que.top().second;
que.pop();
vis[u]=1;
dp[u]=cnt+ceil(a[u]/b[u]);
//printf("当前是第cnt=%lld天,在种u=%lld号坑,dp[%lld]=%lld\n",cnt,u,u,dp[u]);
ans=max(ans,dp[u]);
++cnt;
for(int i=0;i<vec[u].size();++i)
{
int v=vec[u][i];
if(vis[v])continue;
que.push(make_pair(ceil(a[v]/b[v]),v));
}
}
printf("%lld\n",ans);
}
return 0;
}
性质B:
单链可以每棵树依次计算,二分答案需要的天数x,检测x天能否满足每个坑都长够a[i],获得高贵的10分;
#include <bits/stdc++.h>
using namespace std;
using LL = long long;
const int INF = 0x3f3f3f3f;
const LL mod = 1e9 + 7;
const int N = 100005;
int n;
LL a[N], b[N], c[N];
vector<int> G[N];
// 首项 v 公差 d,总和需要至少s,求至少需要的天数
LL cal(LL v, LL d, LL s) {
if (d == 0) return (s + v - 1) / v;
if (d > 0) {
LL l = 1, r = s / v + 1;
while (l < r) {
LL mid = l + r >> 1;
if (mid * v + (__int128)(mid - 1) * mid / 2 * d >= s)
r = mid;
else
l = mid + 1;
}
return l;
}
LL x = (v - 1 + -d - 1) / -d; // 第x+1天开始 都=1
LL s0 = x * v + (x - 1) * x / 2 * d; // 前x天总和,这里不会爆LL
if (s > s0) return x + s - s0;
LL l = 1, r = x;
while (l < r) {
LL mid = l + r >> 1;
if (mid * v + (mid - 1) * mid / 2 * d >= s)
r = mid;
else
l = mid + 1;
}
return l;
}
int main() {
bool case_B = true;
scanf("%d", &n);
for (int i = 1; i <= n; i++) {
scanf("%lld%lld%lld", &a[i], &b[i], &c[i]);
}
for (int i = 1; i < n; i++) {
int u, v;
scanf("%d%d", &u, &v);
G[u].push_back(v);
G[v].push_back(u);
if (u != i || v != i + 1) case_B = false;
}
if (case_B) {
LL ans = 0;
for (int i = 1; i <= n; i++) {
ans = max(ans, i + cal(max(b[i] + i * c[i], 1LL), c[i], a[i]) - 1);
//注意这里每棵树在第i天种下时的增长起点max(b[i] + i * c[i],1),这里x就是i,有助于理解本行代码
}
printf("%lld\n", ans);
} else {
puts("114514"); // 随便猜一个了
}
return 0;
}