2022牛客寒假算法基础集训营6_ACM/NOI/CSP/CCPC/ICPC算法编程高难度练习赛_牛客竞赛OJ
目录
A-回文大师
解题思路:先对数组a求出next数组,数组b存放数组a翻转后的序列,假设匹配到
,那么
构成了一个回文串,此时就找到了一个符合条件的整数 j,对 j 这个位置计数加1。根据kmp算法的性质,字符串匹配的时候实际上会有三个小块对应相等,假设
,那么
,对
计数的同时还要对
等进行计数。将next数组放在一棵树上,把
看作
的父亲,这样
的计数就可以通过
的计数来完成,只需要 1~n 循环一遍就可以将所有的计数更新完成。
AC代码:
#include <iostream>
#include <cstdio>
#include <cstring>
#include <algorithm>
#include <cmath>
#include <cstdlib>
#include <climits>
#include <cctype>
#include <utility>
using namespace std;
typedef long long ll;
const int INF = 0x3f3f3f3f;
const int N = 1000005;
int a[N], b[N], ne[N], ans[N];
int main(){
//freopen("input.txt", "r", stdin);
ios::sync_with_stdio(false);
cin.tie(NULL);
cout.tie(NULL);
int n;
cin >> n;
for(int i = 1; i <= n; i++) cin >> a[i];
for(int i = 2, j = 0; i <= n; i++){
while(j && a[j + 1] != a[i]) j = ne[j];
if(a[j + 1] == a[i]) j++;
ne[i] = j;
}
for(int i = 1; i <= n; i++) b[i] = a[n - i + 1];
for(int i = 1, j = 0; i <= n; i++){
while(j && a[j + 1] != b[i]) j = ne[j];
if(a[j + 1] == b[i]) j++;
ans[j]++;
}
for(int i = n; i >= 1; i--) ans[ne[i]] += ans[i];
for(int i = 1; i < n; i++) cout << ans[i] << ' ';
cout << ans[n] << endl;
return 0;
}
B-价值序列
解题思路:寻找与原序列相同价值的子序列可以考虑在原序列的基础上删去某些元素使得总价值不变。会发现删去一个元素对价值的影响只有两种,要么不变,要么减少。当删去的元素是极值点的时候,价值就会减少;删去的元素不是极值点的时候,价值不变(左右端点如果和旁边的元素不相同,也当作极值点处理)。把连续的相同元素看成一整块,如果这个块的值符合极值点的条件,那么这个块必须有个元素不能删,因此对答案的贡献为;反之,对答案的贡献为
。最后把每个块的答案累乘起来就是总的答案了。
AC代码:
#include <iostream>
#include <cstdio>
#include <cstring>
#include <algorithm>
#include <cmath>
#include <cstdlib>
#include <climits>
#include <cctype>
#include <utility>
using namespace std;
typedef long long ll;
const int INF = 0x3f3f3f3f;
const int mod = 998244353;
const int N = 100005;
ll a[N];
ll quick_pow(ll a, ll b){
ll res = 1;
while(b){
if(b & 1) res = res * a % mod;
a = a * a % mod;
b >>= 1;
}
return res;
}
int main(){
//freopen("input.txt", "r", stdin);
ios::sync_with_stdio(false);
cin.tie(NULL);
cout.tie(NULL);
int t;
cin >> t;
while(t--){
int n;
cin >> n;
for(int i = 1; i <= n; i++) cin >> a[i];
ll ans = 1;
for(int i = 1; i <= n; i++){
int j = i;
while(j < n && a[j + 1] == a[i]) j++;
if(i >= 2 && j <= n - 1 && (a[i] > a[i - 1] && a[i] < a[j + 1] || a[i] < a[i - 1] && a[i] > a[j + 1])){
ans = ans * quick_pow(2, j - i + 1) % mod;
}
else{
ans = ans * ((quick_pow(2, j - i + 1) - 1 + mod) % mod) % mod;
}
i = j;
}
cout << ans << endl;
}
return 0;
}
C-数组划分
解题思路:待补
AC代码:
#include <iostream>
#include <cstdio>
#include <cstring>
#include <algorithm>
#include <cmath>
#include <cstdlib>
#include <climits>
#include <cctype>
#include <utility>
using namespace std;
typedef long long ll;
const int INF = 0x3f3f3f3f;
const int N = 5000005;
int n, q, top;
int l[N], r[N], rk[N], s[N], f[N], ans[N];
ll a[N];
//------------------------
const int BufferSize=1<<16;
char buffer[BufferSize],*head,*tail;
inline char Getchar() {
if(head==tail) {
int l=fread(buffer,1,BufferSize,stdin);
tail=(head=buffer)+l;
}
return *head++;
}
inline int read() {
int x=0,f=1;char c=Getchar();
for(;!isdigit(c);c=Getchar()) if(c=='-') f=-1;
for(;isdigit(c);c=Getchar()) x=x*10+c-'0';
return x*f;
}
void print(int x)
{
if(x>9) print(x/10);
putchar(x%10|'0');
}
//------------------------
int find(int x){
if(f[x] != x) f[x] = find(f[x]);
return f[x];
}
int main(){
//freopen("input.txt", "r", stdin);
ios::sync_with_stdio(false);
cin.tie(NULL);
cout.tie(NULL);
int t;
t = read();
int sn = 0, sq = 0;
while(t--){
n = read(); q = read();
sn += n; sq+= q; top = 0;
for(int i = 1; i <= n; i++){
a[i] = read();
a[i] += a[i - 1];
}
for(int i = 1; i <= q; i++){
l[i] = read();
r[i] = read();
l[i]--;
r[i]--;
}
for(int i = 1; i <= n; i++) f[i] = i;
for(int i = n, j = q; i >= 0; i--){
while(top && a[s[top]] >= a[i]){
f[s[top--]] = i;
}
s[++top] = i;
rk[i] = top;
while(j >= 1 && l[j] == i){
ans[j--] = top - rk[find(r[j])] + 1;
}
}
for(int i = 1; i <= q; i++){
print(ans[i]);
putchar('\n');
}
}
return 0;
}
D-删除子序列
解题思路:本题可以贪心处理,这个方法官方题解中解释的很清楚了。从参赛者的AC代码中发现了另外一种做法,设表示模式串中前 j 个字符作为子序列出现在主串中的个数,那么每次寻找到某个
的时候,如果
不为0,说明前 j 个字符作为子序列出现在主串的个数就可以再加上 1,同时前 j - 1 个字符作为子序列出现在主串的个数就要减掉 1,那么最后
就是题目需要的答案了。
AC代码:
#include <iostream>
#include <cstdio>
#include <cstring>
#include <algorithm>
#include <cmath>
#include <cstdlib>
#include <climits>
#include <cctype>
#include <utility>
using namespace std;
typedef long long ll;
const int INF = 0x3f3f3f3f;
const int N = 1000005;
char s[N], t[N];
int dp[27];
int main(){
//freopen("input.txt", "r", stdin);
ios::sync_with_stdio(false);
cin.tie(NULL);
cout.tie(NULL);
int q;
cin >> q;
while(q--){
int n, m;
cin >> n >> m;
cin >> s + 1 >> t + 1;
memset(dp, 0, sizeof dp);
dp[0] = 0x3f3f3f3f;
for(int i = 1; i <= n; i++){
for(int j = 1; j <= m; j++){
if(s[i] == t[j] && dp[j - 1]){
dp[j - 1]--;
dp[j]++;
}
}
}
cout << dp[m] << endl;
}
return 0;
}
E-骑士
解题思路:为了保证每个骑士没有被秒的可能,那么他的血量和防御之和必须要大于最高攻击的骑士的攻击值。因此按照攻击值对骑士进行排序,每个骑士的生命药剂需要的瓶数就是最高攻击力与他的血量和防御之和的差再加一,即 。最后再特判一下最高攻击的骑士是否能被第二高攻击的骑士秒掉,如果能,答案再加上这样一个差值+1即可。
AC代码:
#include <iostream>
#include <cstdio>
#include <cstring>
#include <algorithm>
#include <cmath>
#include <cstdlib>
#include <climits>
#include <cctype>
#include <utility>
using namespace std;
typedef long long ll;
const int INF = 0x3f3f3f3f;
const int N = 200005;
struct qs{
ll a, b, h;
bool operator < (const qs &u) const{
return a < u.a;
}
}q[N];
int main(){
//freopen("input.txt", "r", stdin);
ios::sync_with_stdio(false);
cin.tie(NULL);
cout.tie(NULL);
int t;
cin >> t;
while(t--){
int n;
cin >> n;
for(int i = 0; i < n; i++) cin >> q[i].a >> q[i].b >> q[i].h;
sort(q, q + n);
ll ans = 0;
ll x = q[n - 1].a;
for(int i = 0; i < n - 1; i++){
if(x >= q[i].b + q[i].h) ans += x - (q[i].b + q[i].h) + 1;
}
if(q[n - 2].a >= q[n - 1].b + q[n - 1].h) ans += q[n - 2].a - (q[n - 1].b + q[n - 1].h) + 1;
cout << ans << endl;
}
return 0;
}
F-+-串
解题思路:为了使 |x| 尽可能小,那也就是让 + 和 - 的数量差距尽可能小。首先统计 + 和 - 的个数,由于每次操作可以使 + 和 - 的数量差距改变 2,因此根据最开始的差距分类判断即可。
如果操作数小于差距的一半,那么操作全部用完就能得到最小的 |x| = x - k * 2;
如果操作数大于等于差距的一半,那么分奇偶讨论:
如果差距最开始是奇数,那么当操作到 |x| = 1 时,剩下的操作数可以保持这个状态;
如果差距最开始是偶数,那么当操作到 |x| = 0 时,剩下的操作数每两次可以回归一次 0,
如果剩下的操作数是偶数,那么最终就是 0;
如果剩下的操作数是奇数,那么一定会多一次使之无法保持 0 的状态,最终就是 2。
AC代码:
#include <iostream>
#include <cstdio>
#include <cstring>
#include <algorithm>
#include <cmath>
#include <cstdlib>
#include <climits>
#include <cctype>
#include <utility>
using namespace std;
typedef long long ll;
const int INF = 0x3f3f3f3f;
int main(){
//freopen("input.txt", "r", stdin);
ios::sync_with_stdio(false);
cin.tie(NULL);
cout.tie(NULL);
int t;
cin >> t;
while(t--){
string s;
cin >> s;
int k;
cin >> k;
int len = s.size();
int cnt1 = 0, cnt2 = 0;
for(int i = 0; i < len; i++){
if(s[i] == '+') cnt1++;
if(s[i] == '-') cnt2++;
}
int x = abs(cnt1 - cnt2);
if(x / 2 <= k){
if(x & 1) cout << 1 << endl;
else cout << (((k - x / 2) & 1) ? 2 : 0) << endl;
}
else cout << x - k * 2 << endl;
}
return 0;
}
G-迷宫2
解题思路:每个点有4个方向的选择,分别是上下左右,枚举这四种选择,如果当前该点的选择和所给的路径中当前位置的选择相同,那么消耗为0,否则消耗为1,题目所求的最小修改次数就转化成了这样一个最小消耗。用 dis 数组存储到每个点的最小消耗,bfs进行搜索,双端队列进行维护,每取出一个队头位置,将消耗为1的新位置放置队尾,将消耗为0的新位置放回队头。由于需要求具体的修改方案,因此用一个pre数组来记录选择哪个方向到达当前点,然后从(n, m)点一步一步倒回(1, 1)点,如果与所给方案不一致,那么就说明在这个点处进行了修改。
AC代码:
#include <iostream>
#include <cstdio>
#include <cstring>
#include <algorithm>
#include <cmath>
#include <cstdlib>
#include <climits>
#include <cctype>
#include <utility>
#include <deque>
using namespace std;
typedef long long ll;
const int INF = 0x3f3f3f3f;
const int N = 1005;
int dx[4] = {0, 0, -1, 1}, dy[4] = {-1, 1, 0, 0};
char dir[4] = {'L', 'R', 'U', 'D'};
char g[N][N];
bool st[N][N]; // st[i][j]存储(i,j)点是否考虑过
int dis[N][N], pre[N][N]; // dis[i][j]存储(1,1)到(i,j)点的消耗
// pre[][]存储上一个位置选择的方向
int n, m;
void bfs(){
dis[1][1] = 0;
deque<pair<int, int> > q;
q.push_back({1, 1});
while(!q.empty()){
int x = q.front().first, y = q.front().second;
q.pop_front();
if(st[x][y]) continue;
st[x][y] = true;
for(int i = 0; i < 4; i++){
int nex = x + dx[i], ney = y + dy[i];
if(nex < 1 || nex > n || ney < 1 || ney > m) continue;
int nedis = dis[x][y] + (dir[i] != g[x][y]);
if(nedis < dis[nex][ney]){
dis[nex][ney] = nedis;
pre[nex][ney] = i;
if(nedis == dis[x][y]) q.push_front({nex, ney});
else q.push_back({nex, ney});
}
}
}
cout << dis[n][m] << endl;
}
int main(){
//freopen("input.txt", "r", stdin);
ios::sync_with_stdio(false);
cin.tie(NULL);
cout.tie(NULL);
int t;
cin >> t;
while(t--){
cin >> n >> m;
for(int i = 1; i <= n; i++) cin >> g[i] + 1;
for(int i = 1; i <= n; i++){
for(int j = 1; j <= m; j++){
dis[i][j] = INF;
st[i][j] = false;
}
}
bfs();
int x = n, y = m;
while(x != 1 || y != 1){
int lastx = x - dx[pre[x][y]], lasty = y - dy[pre[x][y]];
if(g[lastx][lasty] != dir[pre[x][y]])
cout << lastx << ' ' << lasty << ' ' << dir[pre[x][y]] << endl;
x = lastx;
y = lasty;
}
}
return 0;
}
H-寒冬信使2
解题思路:用二进制状压枚举所有出现的组合,0代表b,1代表w。由于每次操作对这样一个二进制的值的大小只会减小不会增加,因此对于每个状态,判断是否能进入更小的必败态,如果能就标记当前状态为必胜态(二进制为0的时候是初始必败态)。那么可以提前对sg函数打个表,然后将所给的初始状态转换成十进制数,判断该数的sg函数是否为0,为0就是必败态,不为0就是必胜态。
AC代码:
#include <iostream>
#include <cstdio>
#include <cstring>
#include <algorithm>
#include <cmath>
#include <cstdlib>
#include <climits>
#include <cctype>
#include <utility>
using namespace std;
typedef long long ll;
const int INF = 0x3f3f3f3f;
const int N = 11;
int sg[1 << N];
char s[N];
void init(){
for(int i = 1; i < (1 << N); i++){
if(i & 1 && !sg[i - 1]){
sg[i] = 1;
continue;
}
for(int j = 1; j < N; j++){
if(i >> j & 1){
for(int k = 0; k < j; k++){
if(!sg[i ^ (1 << j) ^ (1 << k)]) sg[i] = 1;
}
}
}
}
}
int main(){
//freopen("input.txt", "r", stdin);
ios::sync_with_stdio(false);
cin.tie(NULL);
cout.tie(NULL);
int t;
cin >> t;
init();
while(t--){
int n;
cin >> n >> s + 1;
int pos = 0;
for(int i = n; i >= 1; i--){
if(s[i] == 'w') pos = pos << 1 | 1;
else pos = pos << 1;
}
if(sg[pos]) cout << "Yes" << endl;
else cout << "No" << endl;
}
return 0;
}
I-A+B问题
解题思路:用数组存储两个 k 进制数,从最末位开始按位模拟即可。
AC代码:
#include <iostream>
#include <cstdio>
#include <cstring>
#include <algorithm>
#include <cmath>
#include <cstdlib>
#include <climits>
#include <cctype>
#include <utility>
using namespace std;
typedef long long ll;
const int INF = 0x3f3f3f3f;
const int N = 200005;
char a[N], b[N];
int c[N];
int k;
int main(){
//freopen("input.txt", "r", stdin);
ios::sync_with_stdio(false);
cin.tie(NULL);
cout.tie(NULL);
cin >> k >> a >> b;
int alen = strlen(a), blen = strlen(b);
int flag = 0, cnt = 0;
int i = alen - 1, j = blen - 1;
for( ; i >= 0 || j >= 0; i--, j--){
c[cnt++] = ((i >= 0 ? a[i] - '0' : 0) + (j >= 0 ? b[j] - '0' : 0) + flag) % k;
if((i >= 0 ? a[i] - '0' : 0) + (j >= 0 ? b[j] - '0' : 0) + flag >= k) flag = 1;
else flag = 0;
}
if(flag != 0) c[cnt++] = 1;
for(int i = cnt - 1; i >= 0; i--) cout << c[i];
return 0;
}
J-牛妹的数学难题
解题思路:由于每个元素只有0,1,2三种情况,对于连乘式,0 和 1 非常特殊,只要有 0,那么式子就为 0,而 1 的个数对答案也没有影响,因此真正对答案有贡献的只有 2 这种元素。那么对于答案式来说,其本质就是不同的 1 和 2 的选取方案的答案求和。那么可以枚举 1 元素选取的个数 x,每个方案的答案为,最后累加即可。(注意数据量是
大小,因此费马小定理的求逆元阶乘的nlogn方法就会T掉,需要使用递推打表的方式求逆元,然后再循环一次求逆元阶乘)
AC代码:
#include <iostream>
#include <cstdio>
#include <cstring>
#include <algorithm>
#include <cmath>
#include <cstdlib>
#include <climits>
#include <cctype>
#include <utility>
using namespace std;
typedef long long ll;
const int INF = 0x3f3f3f3f;
const int mod = 998244353;
const int N = 10000005;
ll fac[N], invfac[N], pw2[N];
void init(){
fac[0] = invfac[0] = pw2[0] = fac[1] = invfac[1] = 1;
pw2[1] = 2;
for(int i = 2; i < N; i++){
fac[i] = fac[i - 1] * i % mod;
invfac[i] = (mod - mod / i) * invfac[mod % i] % mod;
pw2[i] = pw2[i - 1] * 2 % mod;
}
for(int i = 2; i < N; i++){
invfac[i] = invfac[i] * invfac[i - 1] % mod;
}
}
ll C(ll a, ll b){
if(a < b) return 0;
return fac[a] * invfac[b] % mod * invfac[a - b] % mod;
}
int main(){
//freopen("input.txt", "r", stdin);
ios::sync_with_stdio(false);
cin.tie(NULL);
cout.tie(NULL);
int n, k;
scanf("%d%d", &n, &k);
init();
int cnt1 = 0, cnt2 = 0;
for(int i = 1; i <= n; i++){
int x;
scanf("%d", &x);
if(x == 1) cnt1++;
if(x == 2) cnt2++;
}
ll ans = 0;
int ma = min(cnt1, k);
for(int i = 0; i <= ma; i++){
ans = (ans + C(cnt1, i) * C(cnt2, k - i) % mod * pw2[k - i] % mod) % mod;
}
printf("%lld\n", ans);
return 0;
}