林湾村男子职业技术专修学院 2023年(从10月延了一个礼拜到了)11月ICPC月赛 题目题解与标准代码

由难度从易到难排列。

A 你说的对

如题,奇数输出Yes,偶数输出No即可。

标准代码如下:

#include <bits/stdc++.h>
#define ll long long
#define pii pair<int, int>
using namespace std;
int T;
signed main() {
    ios::sync_with_stdio(false);
    cin.tie(0), cout.tie(0);
    cin >> T;
    if (T & 1) {
        cout << "Yes\n";
    } else
        cout << "No\n";
    return 0;
}

F 字母轮换

可以通过对每个字母轮换 ddd 次的方法完成此题,也可以通过更改错误代码的方式完成此题。通过赛前公告用sha256码隐藏的信息,通过更改错误代码获得AC的首名同学可以获得限定款钥匙扣。

题目中给的错误代码中,有两处错误,一个是if-else的对齐不正确,需要添加大括号;另一个是小写字符的ASCII码的十进制值从az分别为97~122,而 ddd 的数据范围不超过25,这会导致在计算后的编码值会超过127,而保存这个值的数据类型是char,为有符号的8bit整数,当值超过127时,会导致上溢出而变成负数,导致在与z进行比较时,即使超过了z,还会判定成未超过。

改正后的代码如下:

#include"stdio.h"
char s[100005];
int main()
{
    int n,d;
    scanf("%d%d",&n,&d);
    scanf("%s",s);
    for(int i=0;i<n;i++)
    {
        int c=s[i]+d;
        if('a'<=s[i]&&s[i]<='z')
            {if(c>'z')c=c-26;}
        else if('A'<=s[i]&&s[i]<='Z')
            if(c>'Z')c=c-26;
        s[i]=c;
    }
    printf("%s",s);
}

G 一道拥有简洁题面的题

这是一道位运算题,需要先对二进制有一定的了解。

先考虑所有 aia_iai 中,某位为0的数量和为1的数量,这两个数量在异或前后只会受到 kkk 中相对应的位的影响,而不会受到其它的影响。

记这一位在原数组中,0的数量为 n0n_0n0 ,1的数量为 n1n_1n1,在与 kkk 异或之后,0的数量为 m0m_0m01的数量为 m1m_1m1。那么如果 kkk 中对应位为0,则 m0=n0,m1=n1m_0 = n_0, m_1 = n_1m0=n0,m1=n1,反之若 kkk 中对应位为1,则 m0=n1,m1=n0m_0 = n_1, m_1 = n_0m0=n1,m1=n0。又因为要让求和后的结果尽可能大,也就是每一位中与 kkk 做异或之后的1尽可能多,所以如果这一位中 n0>n1n_0>n_1n0>n1 ,则 kkk 中对应位取1,反之取0

该题目时间复杂度为 O(n⋅log109)O(n\cdot log 10^9)O(nlog109),标准代码如下:

#include <bits/stdc++.h>
using namespace std;
void solve(){
    int n;
    cin>>n;
    vector<int> a(n);
    for(int i=0; i<n; i++) cin>>a[i];
    vector<int> w(32, 0);
    int pos;
    for(int i=0; i<n; i++){
        pos=0;
        while(a[i]){
            if(a[i]&1) w[pos]++;
            a[i]>>=1, pos++;
        }
    }
    n/=2;
    int ans=0;
    for(int i=31; i>=0; i--){
        ans<<=1;
        if(w[i]>n) ans+=1;
    }
    cout<<ans<<"\n";
}
int main(){
    ios::sync_with_stdio(0);
    cin.tie(0);
    cout.tie(0);
    int t;
    cin>>t;
    while(t--){
        solve();
    }
    return 0;
}

I xms说数列什么的最简单了

前缀和和差(chā)分是一对可以将区间操作改为单点操作的神奇算法。

观察这样的一个操作过程,如果我想在 aaa 数组上对区间 [l,r][l,r][l,r] 中所有元素的值增加 xxx,我可以开一个全0数组 bbb,令 blb_{l}bl 增加 xxx,令 br+1b_{r+1}br+1 减少 xxx,随后计算数组 ccc,其中 ci=∑j=1ibjc_i = \sum_{j=1}^i b_jci=j=1ibj。我们会发现,ccc数组就是我们要在 aaa 数组上要增加的值。

这个过程可以分为两个步骤,一个是将在 aaa 数组上的区间增加操作转化到在 bbb 数组上的单点修改,另一个是对 bbb 数组的前若干个元素(也就是前缀)求和得到 ccc 数组。而如果有若干组区间修改的操作,则可以在所有操作的第一个步骤执行完之后,一起执行第二个步骤,正确性在这里不加以证明,请自行测试与验证。在第二个步骤中,可以将求和整理成 ci=bi+ci−1c_i = b_i+c_{i-1}ci=bi+ci1 的形式,在线性时间复杂度内完成计算。

这个过程中,我们称 bbb 数组是 ccc 数组的差分数组,ccc 数组是 bbb 数组的前缀和数组。

在本题中,单次操作要求我们在一个区间 [l,r][l,r][l,r] 上增加一个首项为 kkk,公差为 ddd 的数列,这个过程我们可以看做若干区间加操作,分别是在 [l,r][l,r][l,r] 上区间加 kkk,在 [l+1,r][l+1,r][l+1,r] 上区间加 ddd,在 [l+2,r][l+2,r][l+2,r] 上区间加 ddd,在 [l+3,r][l+3,r][l+3,r] 上区间加 ddd,…,在 [r,r][r,r][r,r] 上区间加 ddd,将这些区间加操作转化到 bbb 数组中。转化后在 bbb 数组中可以看做3个操作,分别为,blb_lbl 增加 kkk,在 [l+1,r][l+1,r][l+1,r] 区间上增加 ddd,在 br+1b_{r+1}br+1 减少 k+(r−l)∗dk+(r-l)*dk+(rl)d,这3个操作中,有两个是在 bbb 数组上的单点操作,一个是区间操作,针对这个区间操作,可以再使用上述的方法,给 bbb 数组再开一个差分数组,从而在O(1)时间复杂度内完成一次操作的记录,并在O(n)的时间复杂度内完成数组的复原。

而本题中需依次输出前缀求和的结果,在恢复原数组后,计算过程也可以参考上述的第二个步骤,总体时间复杂度为 O(n+m+q)O(n+m+q)O(n+m+q),标准代码如下:

#include <bits/stdc++.h>
#define ll long long
using namespace std;
const ll N = 2e6 + 5;
const ll M = 1e9 + 7;
ll tt,a[N],df[N],ds[N],n,m,q;
int main() {
	scanf("%lld%lld%lld",&n,&m,&q);
	for (ll i = 1;i <= m;i++) {
		ll l,r,k,d;
		scanf("%lld%lld%lld%lld",&l,&r,&k,&d);
		ds[l] = (ds[l] + k) % M;
		ds[l + 1] = (ds[l + 1] + M - k + d) % M;
		ll p = (k + d * (r - l) % M) % M;
		ds[r + 1] = (ds[r + 1] + 2 * M - p - d) % M;
		ds[r + 2] = (ds[r + 2] + p) % M;
	}
	for (ll i = 1;i <= q;i++) {
		df[i] = (df[i - 1] + ds[i]) % M;
	}
	for (ll i = 1;i <= q;i++) {
		a[i] = (a[i - 1] + df[i]) % M;
	}
	ll ans = 0;
	for (ll i = 1;i <= q;i++) {
		ans = (ans + a[i]) % M;
		printf("%lld\n",ans);
	}
	return 0;
}

J 矩阵的行列式

如题,求给定矩阵的行列式,计算过程在题面中已详细给出,值得注意的是,在对矩阵求行列式时,可以固定选取第 jjj 行元素删除,所以 jjj111 即可。

用递归函数可以快速完成相同问题在不同问题规模下的求解,时间复杂度为 O(n!)O(n!)O(n!) 即可通过此题,标准代码如下:

#include <bits/stdc++.h>
#define int long long
using namespace std;
long long d(int mat[][11],int n){
    if(n == 1){
        return mat[1][1];
    }
    int m[11][11];
    long long res = 0;
    for(int i=1;i<=n;i++){
        for(int j=1;j<=n;j++){
            if(j == i)continue;
            int jj = j;
            if(j>i)jj--;
            for(int k=2;k<=n;k++)m[k-1][jj] = mat[k][j];
        }
        if(i%2)res+=d(m,n-1)*mat[1][i];
        else res-=d(m,n-1)*mat[1][i];
    }
    return res;
}
signed main()
{
    int n;
    cin>>n;
    int m[11][11];
    for(int i=1;i<=n;i++){
        for(int j=1;j<=n;j++){
            cin>>m[i][j];
        }
    }
    cout<<d(m,n)<<endl;
    return 0;
}

H 吉他与孤独与蓝色星球

使用广度优先搜索求最短路径即可,因为保证了每个人最多只有3个朋友,所以当 k=3k=3k=3 时,也最多只会认识39个朋友。因为要避免每次询问都进行全部的搜索,所以当搜索到最短路超过限制时,停止搜索即可。

时间复杂度的上界为 O(39q)O(39q)O(39q),标准代码如下:

#include <bits/stdc++.h>
#define maxn 150005
using namespace std;
vector<int> G[maxn];
int dis[maxn];
int main()
{
	int n, m;
	scanf("%d%d", &n, &m);
	while(m--)
	{
		int a, b;
		scanf("%d%d", &a, &b);
		G[a].push_back(b);
		G[b].push_back(a);
	}
	int Q; scanf("%d", &Q);
	for(int i=1; i<=n; i++) dis[i] = -1;
	while(Q--)
	{
		int x, k;
		scanf("%d%d", &x, &k);
		vector<int> ans;
		queue<int> q;
		q.push(x);
		dis[x] = 0;
		while(!q.empty())
		{
			int v = q.front(); q.pop();
			int d = dis[v];
			if(d <= k) ans.push_back(v);
			if(++d > k) continue;
			for(int u: G[v])
				if(dis[u] == -1)
				{
					dis[u] = d;
					q.push(u);
				}
		}
		int res = 0;
		for(int v: ans)
			res += v, dis[v] = -1;
		printf("%d\n", res);
	}
	return 0;
}

E 闪耀,优骏少女!3

首先观察到,因为每个人都能将消息传递给至少1个人,所以如果将消息传递给所有人所需要帮忙传递的中间人有若干个,那么一开始只需要将消息传递给其中的1个人,就可以在后续的传递过程中,按照任意顺序解锁这些人而不必额外依赖其他人。也就是说,消息在传递过程中,顺序是不会影响这个传递过程的。

然后观察观察数据范围,每个人每次最多将消息传递给除自己之外的100个人,于是我们可以用 O(n)O(n)O(n) 的时间求得,若将消息扩散到 x(1≤x≤100)x(1\leq x \leq 100)x(1x100) 所需的最小时间,记录这个数组。

随后进行动态规划即可,状态转移方程如下:
dpi={p,i=1min(dpi−j+xj),0<j≤100 and i>j dp_i = \left\{\begin{array}{ll} p &, i=1\\\text{min}(dp_{i-j}+x_j)& ,0<j\leq100 \text{ and }i>j\end{array} \right. dpi={pmin(dpij+xj),i=1,0<j100 and i>j
其中,dpidp_idpi 表示消息传递给 iii 个人所需要的最小时间,xjx_jxj 表示通过一次消息传递,将消息传递给其他的 jjj 个人所需要的最小时间,ppp 为主角自己将消息传递给一个人所需要的时间。

这道题目中需要注意的是,主角自己传递给一个人的时间 ppp 应该作为 x1x_1x1 的上界,因为他可以自己将消息传递给其他人;而且 xi+1x_{i+1}xi+1 应当为 xix_{i}xi 的上界,因为若一个人可以在 xi+1x_{i+1}xi+1 时间内将消息传递给 i+1i+1i+1 个人,那么他也可以在 xi+1x_{i+1}xi+1 时间内将消息传递给 iii 个人。这两条限制需要提前满足,随后再进行DP。

该题目时间复杂度为 O(n⋅max(a))O(n\cdot \text{max}(a))O(nmax(a)),标准代码如下:

#include <bits/stdc++.h>
#define int long long
#define INF 0x3f3f3f3f3f3f3f3f
using namespace std;
void solve() {
    int n, p; cin >> n >> p;
    vector<int> a(n + 2, 0), b(n + 2, 0);
    for (int i = 1; i <= n; i++) cin >> a[i];
    for (int i = 1; i <= n; i++) cin >> b[i];
    a[n + 1] = 1, b[n + 1] = p;
    vector<int> mn(101, INF);
    for (int i = 1; i <= n + 1; i++) {
        if (b[i] < mn[a[i]]) mn[a[i]] = b[i];
    }
    int pre = INF;
    for (int i = 100; i; i--) {
        if (pre < mn[i]) mn[i] = pre;
        pre = min(pre, mn[i]);
    }
    vector<int> dp(n + 1, INF);
    dp[1] = p;
    for (int i = 1; i <= 100; i++) {
        for (int j = i + 1; j <= n; j++) { // j - i >= 1
            dp[j] = min(dp[j], dp[j - i] + mn[i]);
        }
    }
    cout << dp[n] << '\n';
}

signed main() {
    ios::sync_with_stdio(0);
    cin.tie(0);
    int T; cin >> T;
    for (; T; T--) solve();
    return 0;
}

C 栏杆染色 - Easy Version

一个一个栏杆地进行染色,肯定可以用 nnn 次操作完成所有栏杆的染色,什么情况下可以减少染色次数呢?

经过观察发现,两个要染较浅颜色相同的栏杆,如果中间所有的栏杆需要染色的颜色都比这个颜色深,那么可以这两个栏杆可以用一次操作,将这个区间都染色成这个较浅的颜色,随后再对中间部分进行染色。所以可以由浅到深地进行染色,深色颜色不影响浅色颜色的染色,但是浅色颜色影响深色颜色的染色。

所以可以使用搜索的方法进行求解,对每个颜色按照上述方法进行搜索,时间复杂度为 O(n⋅max(k))O(n\cdot \text{max}(k))O(nmax(k)),标准代码如下:

#include <bits/stdc++.h>
using namespace std;
void solve(){
    int n,k;
    cin>>n>>k;
    vector<int> a(n);
    for(int i=0;i<n;i++){
        cin>>a[i];
    }
    int res = n;
    for(int i=1;i<=k;i++){
        bool flag = false;
        for(int j=0;j<n;j++){
            if(a[j] == i){
                if(flag)res--;
                flag = true;
            }
            else if(a[j]<i)flag = false;
        }
    }
    cout<<res<<endl;
}
signed main()
{
    ios::sync_with_stdio(false);
    cin.tie(0);
    cout.tie(0);
    int T=1;
    cin>>T;
    while(T--){
        solve();
    }
    return 0;
}

也可以使用单调栈的方法进行求解,从前到后依次尝试将元素入栈:如果栈顶元素大于即将入栈元素,则出栈再次尝试;如果栈顶元素小于即将入栈元素或栈空,则入栈并将染色记录次数加1;如果栈顶元素等于即将入栈元素,则不如栈且停止尝试。因为该算法中,每个元素最多出栈1次且入栈1次,所以该算法时间复杂度为 O(n)O(n)O(n),标准代码如下:

#include <bits/stdc++.h>
using namespace std;
void solve(){
    int n,k;
    cin>>n>>k;
    vector<int> a(n);
    for(int i=0;i<n;i++)cin>>a[i];
    int res = 0;
    vector<int> stk;
    for(int i=0;i<n;i++){
        while(!stk.empty()&&stk[stk.size()-1]>a[i])stk.pop_back();
        if(stk.empty()||stk[stk.size()-1] != a[i]){
            stk.push_back(a[i]);
            res++;
        }
    }
    cout<<res<<endl;
}
signed main()
{
    int T=1;
    cin>>T;
    while(T--){
        solve();
    }
    return 0;
}

D 栏杆染色 - Hard Version

因为存在一段区间可以被染成任意颜色,所以实质是将这段区间左一半和右一半进行拼接,随后再进行与 Easy-Version 中相同的操作,但是即使使用单调栈的方法,时间复杂度依然为 O(nq)O(nq)O(nq),会导致运行超时。

但是在 Easy-Version 中,我们可以观察到,数组从左向右做单调栈和数组从右向左得到的答案应该是相同的,也就是数组的左右对于问题来说是对称的,于是可以进行这样的预处理:像 Easy-Version 中的单调栈算法一样,将数组从左向右执行一遍单调栈,从右向左执行一遍单调栈,分别记录执行到每一个位置时,栈中元素和染色次数。对于每次询问,只需要查询可以染任意色的区间两端的信息,基础的染色次数为两端染色次数之和,如果其中有相同颜色,每个相同颜色对染色次数有-1的贡献。

该算法在预处理过程中,因为需要记录栈中元素,所以预处理的时间复杂度为 O(n⋅max(k))O(n\cdot \text{max}(k))O(nmax(k)),而查询过程中,在比对相同颜色时,也需要遍历每种颜色,所以查询的时间复杂度为 O(q⋅max(k))O(q\cdot \text{max}(k))O(qmax(k)),总时间复杂度为 O((n+q)⋅max(k))O((n+q)\cdot \text{max}(k))O((n+q)max(k)),标准代码如下:

#include <bits/stdc++.h>
using namespace std;
signed main()
{
    int n,k,q;
    cin>>n>>k>>q;
    vector<int> a(n+1);
    for(int i=1;i<=n;i++){
        cin>>a[i];
    }
    vector<vector<bool> > l_has(n+2,vector<bool>(31,false));
    vector<int> l_sum(n+2);
    vector<vector<bool> > r_has(n+2,vector<bool>(31,false));
    vector<int> r_sum(n+2);
    vector<int> stk;
    int s = 0;
    for(int i=1;i<=n;i++){
        while(!stk.empty() && stk[stk.size()-1]>a[i]){
            stk.pop_back();
        }
        if(stk.empty() || stk[stk.size()-1]<a[i]){
            s++;
            stk.push_back(a[i]);
        }
        l_sum[i] = s;
        for(auto x:stk){
            l_has[i][x] = true;
        }
    }
    stk.clear();
    s = 0;
    for(int i=n;i;i--){
        while(!stk.empty() && stk[stk.size()-1]>a[i]){
            stk.pop_back();
        }
        if(stk.empty() || stk[stk.size()-1]<a[i]){
            s++;
            stk.push_back(a[i]);
        }
        r_sum[i] = s;
        for(auto x:stk){
            r_has[i][x] = true;
        }
    }
    while(q--){
        int l,r;
        cin>>l>>r;
        l--;
        r++;
        int res = l_sum[l]+r_sum[r];
        for(int i=1;i<=30;i++){
            if(l_has[l][i] && r_has[r][i])res--;
        }
        cout<<res<<endl;
    }
    return 0;
}

B 攻城掠地

因为询问的是不同的 bbb 数组有多少种,所以观察可能形成的 bbb 数组满足哪些特征:

  1. 因为 bbb 数组表示的是占领城池的机器人编号且至少一个机器人占领,所以 bbb 数组中元素的值 bib_ibi 满足 1≤bi≤n1\leq b_i \leq n1bin
  2. 因为一共有 nnn 个城池,所以数组 bbb 的长度为 nnn

通过两条特征,可以将这个数组看作一张有 nnn 个节点的有向图,数组 bbb 中每个元素 bib_ibi 表示存在一条从节点 iii 到节点 bib_ibi 的有向边。这种图会形成若干环或基环树,下面观察图上的特征:

  1. 图上不存在超过一元的环。因为如果存在这种环,表示对应的机器人进行了“换家”,但是因为执行时有先后顺序的,所以不可能出现这种情况。
  2. 图上基环树的树枝部分不出现分叉。因为一个机器人最多攻击一次其他城池,所以每个机器人最多占领两个城池,所以 bbb 数组中,相同的元素最多出现2次,又因为这种情况下,bbb 机器人一定占领了自己的城池且两个城池都未被其他机器人攻击,所以其中一个元素等于其数组下标。
  3. 因为第一个机器人攻击后,最少有一个机器人的城池被占领后导致被淘汰,所以不能全部都是长度为1的自环。

在综合这两条特征后,这张图应当满足的特征加强为:若干条有向链,链的顶端存在一个自环,和若干独立的自环,独立自环数量可以为0,但是链的数量至少为1

也就是说,最终的 bbb 数组一定要满足的特征为,按照从节点 iii 到节点 bib_ibi 连边的方法,可以整理成上述的图的形式。显然,形成的图与 bbb 数组是一一对应的。

下面证明,任意的满足上述要求的 bbb 数组都是可以通过合法的 aaa 数组和 ppp 排列形成的。

若要证明,只需构造出一种令机器人进攻的顺序即可。先构造每条链的进攻顺序方案,从无自环的一端开始,最头上的节点编号对应的机器人最后执行攻击任意城池,除此之外,从无自环的一端依次向有自环的一段执行,攻击其无自环一端的节点编号对应的城池,就可以满足每条链的结构;再构造独立的自环的攻击方案,这些节点在攻击了其他节点夺取城池之后,夺取的城池又被其他机器人夺走了,所以让这些节点最先攻击,攻击到一个最终会被淘汰的机器人所在的城池即可。

所以只需要统计满足要求的图的数量即可,下面介绍如何计算不同的图的数量,这里图的不同不是指的不同构,而是指的只要存在一条边连接的节点编号不同,就是两张不同的图。

计算方法不唯一,可以使用动态规划的方法,记录 dpi,j(0<j≤i)dp_{i,j}(0< j\leq i)dpi,j(0<ji) 表示总节点数量为 iii 时,链与独立自环的数量为 jjj 时的总方案数量,那么转移方程如下:
dpi,j={1,i=j or j=1dpi−1,j⋅(i+j)+dpi−1,j−1,other dp_{i,j} = \left\{\begin{array}{ll} 1 &,i=j\text{ or }j = 1 \\dp_{i-1,j}\cdot (i+j)+dp_{i-1,j-1} & ,other\end{array} \right. dpi,j={1dpi1,j(i+j)+dpi1,j1,i=j or j=1,other
节点数量为 nnn 时,总方案数 resnres_nresn 的计算方式如下:
resn=∑i=1ndpn,i−1 res_n = \sum_{i=1}^ndp_{n,i}-1 resn=i=1ndpn,i1
因为计算过程中只存在加法和乘法,所以可以对任意数取模。

在预处理过程中进行动态规划,预先求出所有节点数量的答案,预处理的时间复杂度为 O(max(n)2)O(\text{max}(n)^2)O(max(n)2),查询时直接输出结果即可,查询的时间复杂度为 O(q)O(q)O(q),总时间复杂度为 O(max(n)2+q)O(\text{max}(n)^2+q)O(max(n)2+q)。标准代码如下:

#include <bits/stdc++.h>
#define int long long
using namespace std;
vector<int> res;
int mod;
void solve(){
    res.push_back(1);
    int n = 5000;
    vector<int> chain(1,1);
    int fun=1;
    for(int i=1;i<=n;i++){
        chain.push_back(chain[i-1]);
        for(int j=i-1;j;j--){
            chain[j] = (chain[j]*(i-1+j)%mod+chain[j-1])%mod;
        }
        chain[0] = 0;
        int r = mod-1;
        for(int j=0;j<chain.size();j++){
            r = (r+chain[j])%mod;
        }
        res.push_back(r);
    }
}
signed main()
{
    int T=1;
    cin>>T>>mod;
    solve();
    int q;
    while(T--){
        cin>>q;
        cout<<res[q]<<endl;
    }
    return 0;
}
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值