由难度从易到难排列。
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 字母轮换
可以通过对每个字母轮换
d
d
d 次的方法完成此题,也可以通过更改错误代码的方式完成此题。通过赛前公告用sha256码隐藏的信息,通过更改错误代码获得AC
的首名同学可以获得限定款钥匙扣。
题目中给的错误代码中,有两处错误,一个是if-else的对齐不正确,需要添加大括号;另一个是小写字符的ASCII码的十进制值从a
到z
分别为97
~122
,而
d
d
d 的数据范围不超过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 一道拥有简洁题面的题
这是一道位运算题,需要先对二进制有一定的了解。
先考虑所有
a
i
a_i
ai 中,某位为0
的数量和为1
的数量,这两个数量在异或前后只会受到
k
k
k 中相对应的位的影响,而不会受到其它的影响。
记这一位在原数组中,0
的数量为
n
0
n_0
n0 ,1
的数量为
n
1
n_1
n1,在与
k
k
k 异或之后,0
的数量为
m
0
m_0
m0,1
的数量为
m
1
m_1
m1。那么如果
k
k
k 中对应位为0
,则
m
0
=
n
0
,
m
1
=
n
1
m_0 = n_0, m_1 = n_1
m0=n0,m1=n1,反之若
k
k
k 中对应位为1
,则
m
0
=
n
1
,
m
1
=
n
0
m_0 = n_1, m_1 = n_0
m0=n1,m1=n0。又因为要让求和后的结果尽可能大,也就是每一位中与
k
k
k 做异或之后的1尽可能多,所以如果这一位中
n
0
>
n
1
n_0>n_1
n0>n1 ,则
k
k
k 中对应位取1
,反之取0
。
该题目时间复杂度为 O ( n ⋅ l o g 1 0 9 ) O(n\cdot log 10^9) O(n⋅log109),标准代码如下:
#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ā)分是一对可以将区间操作改为单点操作的神奇算法。
观察这样的一个操作过程,如果我想在 a a a 数组上对区间 [ l , r ] [l,r] [l,r] 中所有元素的值增加 x x x,我可以开一个全0数组 b b b,令 b l b_{l} bl 增加 x x x,令 b r + 1 b_{r+1} br+1 减少 x x x,随后计算数组 c c c,其中 c i = ∑ j = 1 i b j c_i = \sum_{j=1}^i b_j ci=∑j=1ibj。我们会发现, c c c数组就是我们要在 a a a 数组上要增加的值。
这个过程可以分为两个步骤,一个是将在 a a a 数组上的区间增加操作转化到在 b b b 数组上的单点修改,另一个是对 b b b 数组的前若干个元素(也就是前缀)求和得到 c c c 数组。而如果有若干组区间修改的操作,则可以在所有操作的第一个步骤执行完之后,一起执行第二个步骤,正确性在这里不加以证明,请自行测试与验证。在第二个步骤中,可以将求和整理成 c i = b i + c i − 1 c_i = b_i+c_{i-1} ci=bi+ci−1 的形式,在线性时间复杂度内完成计算。
这个过程中,我们称 b b b 数组是 c c c 数组的差分数组, c c c 数组是 b b b 数组的前缀和数组。
在本题中,单次操作要求我们在一个区间 [ l , r ] [l,r] [l,r] 上增加一个首项为 k k k,公差为 d d d 的数列,这个过程我们可以看做若干区间加操作,分别是在 [ l , r ] [l,r] [l,r] 上区间加 k k k,在 [ l + 1 , r ] [l+1,r] [l+1,r] 上区间加 d d d,在 [ l + 2 , r ] [l+2,r] [l+2,r] 上区间加 d d d,在 [ l + 3 , r ] [l+3,r] [l+3,r] 上区间加 d d d,…,在 [ r , r ] [r,r] [r,r] 上区间加 d d d,将这些区间加操作转化到 b b b 数组中。转化后在 b b b 数组中可以看做3个操作,分别为, b l b_l bl 增加 k k k,在 [ l + 1 , r ] [l+1,r] [l+1,r] 区间上增加 d d d,在 b r + 1 b_{r+1} br+1 减少 k + ( r − l ) ∗ d k+(r-l)*d k+(r−l)∗d,这3个操作中,有两个是在 b b b 数组上的单点操作,一个是区间操作,针对这个区间操作,可以再使用上述的方法,给 b b b 数组再开一个差分数组,从而在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 矩阵的行列式
如题,求给定矩阵的行列式,计算过程在题面中已详细给出,值得注意的是,在对矩阵求行列式时,可以固定选取第 j j j 行元素删除,所以 j j j 取 1 1 1 即可。
用递归函数可以快速完成相同问题在不同问题规模下的求解,时间复杂度为 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
=
3
k=3
k=3 时,也最多只会认识39
个朋友。因为要避免每次询问都进行全部的搜索,所以当搜索到最短路超过限制时,停止搜索即可。
时间复杂度的上界为 O ( 39 q ) 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(1≤x≤100) 所需的最小时间,记录这个数组。
随后进行动态规划即可,状态转移方程如下:
d
p
i
=
{
p
,
i
=
1
min
(
d
p
i
−
j
+
x
j
)
,
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(dpi−j+xj),i=1,0<j≤100 and i>j
其中,
d
p
i
dp_i
dpi 表示消息传递给
i
i
i 个人所需要的最小时间,
x
j
x_j
xj 表示通过一次消息传递,将消息传递给其他的
j
j
j 个人所需要的最小时间,
p
p
p 为主角自己将消息传递给一个人所需要的时间。
这道题目中需要注意的是,主角自己传递给一个人的时间 p p p 应该作为 x 1 x_1 x1 的上界,因为他可以自己将消息传递给其他人;而且 x i + 1 x_{i+1} xi+1 应当为 x i x_{i} xi 的上界,因为若一个人可以在 x i + 1 x_{i+1} xi+1 时间内将消息传递给 i + 1 i+1 i+1 个人,那么他也可以在 x i + 1 x_{i+1} xi+1 时间内将消息传递给 i i i 个人。这两条限制需要提前满足,随后再进行DP。
该题目时间复杂度为 O ( n ⋅ max ( a ) ) O(n\cdot \text{max}(a)) O(n⋅max(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
一个一个栏杆地进行染色,肯定可以用 n n n 次操作完成所有栏杆的染色,什么情况下可以减少染色次数呢?
经过观察发现,两个要染较浅颜色相同的栏杆,如果中间所有的栏杆需要染色的颜色都比这个颜色深,那么可以这两个栏杆可以用一次操作,将这个区间都染色成这个较浅的颜色,随后再对中间部分进行染色。所以可以由浅到深地进行染色,深色颜色不影响浅色颜色的染色,但是浅色颜色影响深色颜色的染色。
所以可以使用搜索的方法进行求解,对每个颜色按照上述方法进行搜索,时间复杂度为 O ( n ⋅ max ( k ) ) O(n\cdot \text{max}(k)) O(n⋅max(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 ( n q ) O(nq) O(nq),会导致运行超时。
但是在 Easy-Version 中,我们可以观察到,数组从左向右做单调栈和数组从右向左得到的答案应该是相同的,也就是数组的左右对于问题来说是对称的,于是可以进行这样的预处理:像 Easy-Version 中的单调栈算法一样,将数组从左向右执行一遍单调栈,从右向左执行一遍单调栈,分别记录执行到每一个位置时,栈中元素和染色次数。对于每次询问,只需要查询可以染任意色的区间两端的信息,基础的染色次数为两端染色次数之和,如果其中有相同颜色,每个相同颜色对染色次数有-1
的贡献。
该算法在预处理过程中,因为需要记录栈中元素,所以预处理的时间复杂度为 O ( n ⋅ max ( k ) ) O(n\cdot \text{max}(k)) O(n⋅max(k)),而查询过程中,在比对相同颜色时,也需要遍历每种颜色,所以查询的时间复杂度为 O ( q ⋅ max ( k ) ) O(q\cdot \text{max}(k)) O(q⋅max(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 攻城掠地
因为询问的是不同的 b b b 数组有多少种,所以观察可能形成的 b b b 数组满足哪些特征:
- 因为 b b b 数组表示的是占领城池的机器人编号且至少一个机器人占领,所以 b b b 数组中元素的值 b i b_i bi 满足 1 ≤ b i ≤ n 1\leq b_i \leq n 1≤bi≤n。
- 因为一共有 n n n 个城池,所以数组 b b b 的长度为 n n n。
通过两条特征,可以将这个数组看作一张有 n n n 个节点的有向图,数组 b b b 中每个元素 b i b_i bi 表示存在一条从节点 i i i 到节点 b i b_i bi 的有向边。这种图会形成若干环或基环树,下面观察图上的特征:
- 图上不存在超过一元的环。因为如果存在这种环,表示对应的机器人进行了“换家”,但是因为执行时有先后顺序的,所以不可能出现这种情况。
- 图上基环树的树枝部分不出现分叉。因为一个机器人最多攻击一次其他城池,所以每个机器人最多占领两个城池,所以
b
b
b 数组中,相同的元素最多出现
2
次,又因为这种情况下, b b b 机器人一定占领了自己的城池且两个城池都未被其他机器人攻击,所以其中一个元素等于其数组下标。 - 因为第一个机器人攻击后,最少有一个机器人的城池被占领后导致被淘汰,所以不能全部都是长度为
1
的自环。
在综合这两条特征后,这张图应当满足的特征加强为:若干条有向链,链的顶端存在一个自环,和若干独立的自环,独立自环数量可以为0
,但是链的数量至少为1
。
也就是说,最终的 b b b 数组一定要满足的特征为,按照从节点 i i i 到节点 b i b_i bi 连边的方法,可以整理成上述的图的形式。显然,形成的图与 b b b 数组是一一对应的。
下面证明,任意的满足上述要求的 b b b 数组都是可以通过合法的 a a a 数组和 p p p 排列形成的。
若要证明,只需构造出一种令机器人进攻的顺序即可。先构造每条链的进攻顺序方案,从无自环的一端开始,最头上的节点编号对应的机器人最后执行攻击任意城池,除此之外,从无自环的一端依次向有自环的一段执行,攻击其无自环一端的节点编号对应的城池,就可以满足每条链的结构;再构造独立的自环的攻击方案,这些节点在攻击了其他节点夺取城池之后,夺取的城池又被其他机器人夺走了,所以让这些节点最先攻击,攻击到一个最终会被淘汰的机器人所在的城池即可。
所以只需要统计满足要求的图的数量即可,下面介绍如何计算不同的图的数量,这里图的不同不是指的不同构,而是指的只要存在一条边连接的节点编号不同,就是两张不同的图。
计算方法不唯一,可以使用动态规划的方法,记录
d
p
i
,
j
(
0
<
j
≤
i
)
dp_{i,j}(0< j\leq i)
dpi,j(0<j≤i) 表示总节点数量为
i
i
i 时,链与独立自环的数量为
j
j
j 时的总方案数量,那么转移方程如下:
d
p
i
,
j
=
{
1
,
i
=
j
or
j
=
1
d
p
i
−
1
,
j
⋅
(
i
+
j
)
+
d
p
i
−
1
,
j
−
1
,
o
t
h
e
r
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={1dpi−1,j⋅(i+j)+dpi−1,j−1,i=j or j=1,other
节点数量为
n
n
n 时,总方案数
r
e
s
n
res_n
resn 的计算方式如下:
r
e
s
n
=
∑
i
=
1
n
d
p
n
,
i
−
1
res_n = \sum_{i=1}^ndp_{n,i}-1
resn=i=1∑ndpn,i−1
因为计算过程中只存在加法和乘法,所以可以对任意数取模。
在预处理过程中进行动态规划,预先求出所有节点数量的答案,预处理的时间复杂度为 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;
}