The 2022 ICPC Asia Nanjing Regional Contest
写在前面
补题地址:https://codeforces.com/gym/104128。
以下按个人向难度排序。
SUA 什么牛逼提妈的又被斩杀了,wenqizhi 大爹一个人爆切三道我和 dztlb 大神两个人分别在 AM 上调不出来坐牢真是纯纯的战犯
题很好但是我很不好呜呜呜呜
I 签到
显然当且仅当所有字符均相等才为完美回文。
字符集只有 26,直接做就行了。
#include <bits/stdc++.h>
using namespace std;
#define int long long
const int N=1e6+5;
int T;
char s[N];
int a[300];
signed main(){
cin>>T;
while(T--){
scanf("%s",s+1);
int n=strlen(s+1);
for(int i=0;i<=100;++i) a[i]=0;
for(int i=1;i<=n;++i){
a[s[i]-'a']++;
}
int ans=ans=n;
for(int i=0;i<26;++i){
ans=min(ans,n-a[i]);
}
cout<<ans<<'\n';
}
return 0;
}
G 贪心,模拟
显然应当在保证数量大于 0 情况下,尽可能进行操作 -1。
发现这个操作对答案的分数有加有减的很麻烦啊,一个套路是考虑统一操作的形式。考虑先将所有操作均看做一次操作 1(即令分母分子同时加 1),发现操作 -1 可看成再额外对分子减 1,对分母减 2。考虑最终进行了 \(c\) 次操作 -1,则进行 \(n\) 次操作后,最终的答案可以表示为:
保证上式分母时刻大于 0 即对应一种合法的操作方案。于是一个显然的想法是考虑直接贪心,尽量进行操作 -1,若发现此时进行操作 -1 后会导致上式不合法,则尝试将之前一次操作 -1 反悔即可。
赛时 wenqizhi 的思路是直接模拟上述转化,先将所有操作 0 均看做操作 1,然后考虑将当前的分母的值看做一张折线图,然后再倒序枚举折线图上的点,若当前点对应操作为 0,且在此之后的折线图上最小值至少为 2,则可以将当前的操作贪心地更改为操作 -1。
时间复杂度均为 \(O(n)\) 级别。
code by wenqizhi:
#include<bits/stdc++.h>
using namespace std;
#define ll long long
#define ull unsigned long long
int read()
{
int x = 0; bool f = false; char c = getchar();
while(c < '0' || c > '9') f |= (c == '-'), c = getchar();
while(c >= '0' && c <= '9') x = (x << 1) + (x << 3) + (c & 15), c = getchar();
return f ? -x : x;
}
const int N = 1e6 + 5;
int n, a[N], pre[N], sufmn[N];
void solve()
{
n = read();
int b = 0;
for(int i = 1; i <= n; ++i)
{
a[i] = read();
pre[i] = pre[i - 1] + ((a[i] == 0) ? 1 : a[i]), b += (a[i] == -1);
}
sufmn[n] = pre[n];
if(pre[n] < 0){ printf("-1\n"); return ; }
for(int i = n - 1; i >= 1; --i)
{
sufmn[i] = min(pre[i], sufmn[i + 1]);
if(pre[i] < 0)
{
printf("-1\n");
return ;
}
}
int cnt = 0;
sufmn[n + 1] = 0x7fffffff;
for(int i = n; i >= 1; --i)
{
sufmn[i] = min(sufmn[i], sufmn[i + 1]);
if(a[i] == 0)
{
if(sufmn[i] >= 2) ++cnt, sufmn[i] -= 2;
}
}
int X = n - b - cnt + 1, Y = n - 2 * b - 2 * cnt + 1, mx = max(X, Y);
for(int i = 2; i <= mx; ++i)
while(X % i == 0 && Y % i == 0) X /= i, Y /= i;
printf("%d %d\n", X, Y);
}
int main()
{
int T = read();
while(T--) solve();
return 0;
}
反悔贪心:
//
/*
By:Luckyblock
*/
#include <bits/stdc++.h>
#define LL long long
const int kN = 1e6 + 10;
//=============================================================
int n;
//=============================================================
void getans(int ans_) {
int x = n + 1 - ans_, y = n + 1 - 2 * ans_;
int d = std::__gcd(x, y);
std::cout << x / d << " " << y / d << "\n";
}
//=============================================================
int main() {
// freopen("1.txt", "r", stdin);
std::ios::sync_with_stdio(0), std::cin.tie(0);
int T; std::cin >> T;
while (T --) {
std::cin >> n;
int cnt = 0, ans = 0, flag = 0;
for (int i = 1; i <= n; ++ i) {
int a; std::cin >> a;
if (a == 1) {
continue;
} else if (a == -1) {
if (2 * (ans + 1) < i + 1) ++ ans;
else if (cnt) -- cnt;
else flag = 1;
} else {
if (2 * (ans + 1) < i + 1) ++ ans, ++ cnt;
}
}
if (flag) std::cout << -1 << "\n";
else getans(ans);
}
return 0;
}
D 二分答案,枚举
显然答案有单调性,考虑二分答案。问题变为能否使序列中不小于 \(\operatorname{mid}\) 的数的数量不小于 \(k\)。
然后考虑被枚举操作的区间,仅需考虑区间中的 0 中有多少可以变成 1 即可。对于操作区间为 \([1, m]\) 时可以大力处理,然后发现当操作区间右移至 \([i, i + m - 1]\) 时,新加入的数 \(a_{i+m-1}\) 会变为 \(a_{i+m-1} + kd\),原区间内的数均会减 \(d\)。
发现每一个数在操作区间内时,随区间右移其值是单调递减的,即每个数均会在操作区间左端点取 \(i\) 时变为不小于 \(k\),又会在操作区间左端点取 \(i'(i'>i)\) 时变为小于 \(k\),于是考虑将每个数的贡献 \(\plusmn 1\) 分别挂在这两个位置上,则每种操作区间的影响即可通过前缀和求得。
赛时 wenqizhi 大神原理大致如上但是用线段树实现的,一发过了但是看不懂呃呃呃呃:
code by wenqizhi:
#include<bits/stdc++.h>
using namespace std;
#define ll long long
#define ull unsigned long long
int read()
{
int x = 0; bool f = false; char c = getchar();
while(c < '0' || c > '9') f |= (c == '-'), c = getchar();
while(c >= '0' && c <= '9') x = (x << 1) + (x << 3) + (c & 15), c = getchar();
return f ? -x : x;
}
const int N = 2e5 + 5;
int n, m, K;
ll c, d, a[N], A[N], b[N];
#define ls(x) (x << 1)
#define rs(x) (x << 1 | 1)
struct Segment
{
int sum[N << 2];
void update(int k, int l, int r, int pos, int val)
{
sum[k] += val;
if(l == r) return ;
int mid = (l + r) >> 1;
if(pos <= mid) update(ls(k), l, mid, pos, val);
else update(rs(k), mid + 1, r, pos, val);
}
int query(int k, int l, int r, int L, int R)
{
if(L <= l && r <= R) return sum[k];
int mid = (l + r) >> 1;
if(R <= mid) return query(ls(k), l, mid, L, R);
if(L > mid) return query(rs(k), mid + 1, r, L, R);
return query(ls(k), l, mid, L, R) + query(rs(k), mid + 1, r, L, R);
}
}T1, T2;
ll lazy;
int nn, mm;
bool check(ll mid)
{
int pos1 = upper_bound(A + 1, A + mm + 1, mid) - A - 1;
int pos2 = upper_bound(b + 1, b + nn + 1, mid - lazy) - b - 1;
int sum1 = T1.query(1, 1, mm, 1, pos1), sum2 = T2.query(1, 1, nn, 1, pos2);
if(sum1 + sum2 >= n - K + 1) return true;
else return false;
}
int main()
{
n = read(), K = read(), m = read(), c = read(), d = read();
for(int i = 1; i <= n; ++i) a[i] = read(), b[i] = a[i] + c + d * (i - 1), A[i] = a[i];
b[n + 1] = -0x7fffffffffffffff, b[n + 2] = 0x7fffffffffffffff;
sort(b + 1, b + n + 3);
nn = unique(b + 1, b + n + 3) - (b + 1);
A[n + 1] = -0x7fffffffffffffff, A[n + 2] = 0x7fffffffffffffff;
sort(A + 1, A + n + 3);
mm = unique(A + 1, A + n + 3) - (A + 1);
for(int i = 1; i <= m; ++i)
{
int id = lower_bound(b + 1, b + nn + 1, a[i] + c + d * (i - 1)) - b;
T2.update(1, 1, nn, id, 1);
}
for(int i = m + 1; i <= n; ++i)
{
int id = lower_bound(A + 1, A + mm + 1, a[i]) - A;
T1.update(1, 1, mm, id, 1);
}
ll ans = -0x7fffffffffffffff;
for(int i = m; i <= n; ++i)
{
ll l = -0x7ffffffffffff, r = 0x7ffffffffffff;
while(l < r)
{
ll mid = (l + r) >> 1;
if(check(mid)) r = mid;
else l = mid + 1;
}
// printf("i = %d, l = %lld\n", i, l);
ans = max(ans, l);
if(i < n)
{
lazy -= d;
int lastid = i - m + 1;
int id = lower_bound(b + 1, b + nn + 1, a[lastid] + c + d * (lastid - 1)) - b;
T2.update(1, 1, nn, id, -1);
id = lower_bound(A + 1, A + mm + 1, a[lastid]) - A;
T1.update(1, 1, mm, id, 1);
lastid = i + 1;
id = lower_bound(b + 1, b + nn + 1, a[lastid] + c + d * (lastid - 1)) - b;
T2.update(1, 1, nn, id, 1);
id = lower_bound(A + 1, A + mm + 1, a[lastid]) - A;
T1.update(1, 1, mm, id, -1);
}
}
printf("%lld\n", ans);
return 0;
}
A 枚举,结论,二维前缀和
和 wenqizhi 大神讨论了下就直接会写了然而直到结束吃了 11 发都没过赛后一看是超级脑瘫错误呃呃,负数乘负数等于正数!!!
先考虑若没有坑,则最终剩下的袋鼠一定是一个子矩形,对比原矩形上下左右减少的长度,即从对应方向跳的最大步数,则模拟一下操作序列即得最终剩下的袋鼠对应原矩形的子矩形的范围。
然后考虑有坑的情况,这种相对运动的情况,一个显然的想法是不考虑有多少袋鼠跳进了坑,而是考虑坑在移动且经过了多少袋鼠,考虑将操作反向然后顺序模拟一下操作序列即可得到其运动轨迹,即可标记出相对于坑初始位置,有哪些位置的袋鼠进了坑。
发现最终剩下的袋鼠,即上述子矩阵中的袋鼠,减去该子矩阵中的袋鼠进坑的数量。考虑对上述预处理的,相对于坑初始位置,有哪些位置的袋鼠进了坑,做一个二维前缀和,即可很方便地查询相对于坑的初始位置,位于某个矩形内有多少袋鼠进了坑。
于是大力枚举坑的初始位置,二维前缀和查询上述子矩形中有多少袋鼠进了坑即可。总时间复杂度 \(O(nm)\) 级别,具体如何预处理以及各种边界详见代码。
特别注意:特判最终剩下的袋鼠的子矩形不存在的情况!若某一维上跳出的范围大于这一维的长度即不存在!
//
/*
By:Luckyblock
*/
#include <bits/stdc++.h>
#define LL long long
const int kN = 1e3 + 10;
//=============================================================
int n, m, k;
std::string s;
int cnt[kN << 1][kN << 1];
int d, len, delta[5];
std::map <char, int> ex, ey;
//=============================================================
void init() {
d = std::max(n, m) + 2;
for (int i = -n - 2; i <= n + 2; ++ i) {
for (int j = -m - 2; j <= m + 2; ++ j) {
cnt[i + d][j + d] = 0;
}
}
len = s.length() - 1;
cnt[0 + d][0 + d] = 1;
delta[0] = delta[1] = delta[2] = delta[3] = 0;
int x = 0, y = 0;
for (int i = 1; i <= len; ++ i) {
x -= ex[s[i]], y -= ey[s[i]];
delta[0] = std::min(delta[0], x);
delta[1] = std::max(delta[1], x);
delta[2] = std::min(delta[2], y);
delta[3] = std::max(delta[3], y);
if (x > - n && y > - m && x < n && y < m) {
cnt[x + d][y + d] = 1;
}
}
for (int i = -n; i <= n; ++ i) {
for (int j = -m; j <= m; ++ j) {
cnt[i + d][j + d] += cnt[i - 1 + d][j + d] +
cnt[i + d][j - 1 + d] -
cnt[i - 1 + d][j - 1 + d];
}
}
}
//=============================================================
int main() {
// freopen("1.txt", "r", stdin);
std::ios::sync_with_stdio(0), std::cin.tie(0);
ex['U'] = -1, ex['D'] = 1, ex['L'] = 0, ex['R'] = 0;
ey['U'] = 0, ey['D'] = 0, ey['L'] = -1, ey['R'] = 1;
int T; std::cin >> T;
while (T --) {
std::cin >> n >> m >> k;
std::cin >> s; s = "$" + s;
init();
int ans = 0;
int c1 = (n + delta[0] - delta[1]) * (m + delta[2] - delta[3]); //负数乘负数等于正数
if (n + delta[0] - delta[1] <= 0 || m + delta[2] - delta[3] <= 0) c1 = 0;
if (c1 <= 0) {
std::cout << (k == 0 ? n * m : 0) << "\n";
continue;
}
for (int i = 1; i <= n; ++ i) {
for (int j = 1; j <= m; ++ j) {
int c3 = cnt[(n - i) + delta[0] + d][(m - j) + delta[2] + d] -
cnt[(n - i) + delta[0] + d][-(j - 1) - 1 + delta[3] + d] -
cnt[-(i - 1) - 1 + delta[1] + d][(m - j) + delta[2] + d] +
cnt[-(i - 1) - 1 + delta[1] + d][-(j - 1) - 1 + delta[3] + d];
if (c1 - c3 == k) {
++ ans;
}
}
}
std::cout << ans << "\n";
}
return 0;
}
/*
1
19 12 0
UDLDDUUUUDDDLLRDUDUURULUUUDRDUDRDRLRLRLULULLLDLDDRLUUUURUUUDDRLLRUUUDULURUULLRDRLRDDURDUUURRRLURLRUULRRUDURDLUUURDLURDDLUUURDDRLLURRDLRUDLRDRLLRRDRDDLDRURRRLUDULLLRUUDLRRURRDLLRRRDLLRDDDLRLRURURDDDL
1
3 3 4
LU
1
4 5 0
UUUUUUU
1
4 5 3
ULDDRR
*/
B DP,枚举
我和 dztlb 大神双人红温的时候,wenqizhi 大爹跑去开见面会了,然而到他回来我们也没调出来然后他一个人随便写写就又一发把这题过了唉唉真大神吧
先考虑不带修怎么做,显然可以得到一个 \(O(n)\) 的 DP,记 \(f_{i}\) 表示对于 \(0\sim i\) 的合法建设方案,钦定位置 \(i\) 必选时的最小花费,则有显然的区间转移形式,套路地单调队列优化即可。设 \(a_{0}=a_{n+1}=0\),则答案即为 \(f_{n+1}\)。
注意上述 DP 时要特判必须建设的位置,发现仅需完成该位置的转移后,直接清空单调队列并仅插入该位置即可,从而保证之后的所有位置只能从该位置的状态转移。
然后考虑带修,每次修改独立,一个套路的想法是考虑枚举被修改位置 \(i\) 后面若干位置 \(j\)(发现至多 \(k\) 个,因为显然长度为 \(k\) 的区间内至少有一个位置被建设),并检查若 \(j\) 作为 \(i\) 之后的第一个建设的位置时的最小代价。
考虑额外预处理 \(g_i\) 表示对于 \(i\sim n+1\) 的合法建设方案,钦定位置 \(i\) 必选时的最小花费,则修改 \(i\) 不影响 \(g_{i+1}\sim g_{n+1}\),仅会影响 \(f_{i}\sim f_{i+k-1}\),于是仅需重算这些位置即可,答案即为:
重算同样单调队列即可,则单次询问 \(O(k)\) 级别,总时间复杂度 \(O(n + qk)\) 级别。
code by wenqizhi:
#include<bits/stdc++.h>
using namespace std;
#define ll long long
#define ull unsigned long long
int read()
{
int x = 0; bool f = false; char c = getchar();
while(c < '0' || c > '9') f |= (c == '-'), c = getchar();
while(c >= '0' && c <= '9') x = (x << 1) + (x << 3) + (c & 15), c = getchar();
return f ? -x : x;
}
const ll inf = 0x7ffffffffffffff;
const int N = 5e5 + 5;
int n, K, Q, vis[N];
ll a[N], b[N];
ll dp1[N], dp2[N], dp[N];
int q[N], l, r;
char s[N];
vector<int> pos;
void solve()
{
n = read(), K = read();
a[0] = a[n + 1] = 0;
for(int i = 1; i <= n; ++i) a[i] = read(), dp1[i] = dp2[i] = inf, b[n - i + 1] = a[i];
scanf("%s", s + 1);
pos.clear();
pos.emplace_back(0);
for(int i = 1; i <= n; ++i)
{
vis[i] = s[i] - '0';
if(vis[i]) pos.emplace_back(i);
}
pos.emplace_back(n + 1);
dp1[0] = dp2[0] = 0;
l = 1, r = 0, q[++r] = 0;
for(int i = 1; i <= n + 1; ++i)
{
while(l <= r && q[l] < i - K) ++l;
dp1[i] = dp1[q[l]] + a[i];
while(l <= r && (vis[i] || dp1[q[r]] >= dp1[i])) --r;
q[++r] = i;
}
l = 1, r = 0, q[++r] = 0;
for(int i = 1; i <= n + 1; ++i)
{
while(l <= r && q[l] < i - K) ++l;
dp2[i] = dp2[q[l]] + b[i];
while(l <= r && (vis[n - i + 1] || dp2[q[r]] >= dp2[i])) --r;
q[++r] = i;
}
reverse(dp2, dp2 + n + 2);
Q = read();
while(Q--)
{
int p = read();
ll val = read();
int R = lower_bound(pos.begin(), pos.end(), p) - pos.begin();
int L = R - 1;
ll ans = inf;
if(pos[R] == p)
{
printf("%lld\n", dp1[n + 1] - a[p] + val);
continue;
}
L = pos[L], R = pos[R];
L = max(L, p - K), R = min(R, p + K);
// printf("p = %d, val = %lld, L = %d, R = %d\n", p, val, L, R);
l = 1, r = 0;
for(int i = L; i <= R; ++i) dp[i] = dp1[i];
for(int i = L; i < p; ++i)
{
while(l <= r && dp[q[r]] >= dp[i]) --r;
q[++r] = i;
}
for(int i = p; i <= R; ++i)
{
while(l <= r && q[l] < i - K) ++l;
dp[i] = dp[q[l]] + (i == p ? val : a[i]);
ans = min(ans, dp[i] + dp2[i] - a[i]);
while(l <= r && (vis[i] || dp[q[r]] >= dp[i])) --r;
q[++r] = i;
}
printf("%lld\n", ans);
}
}
int main()
{
int T = read();
while(T--) solve();
return 0;
}
M 计算几何,枚举,大力讨论
沟槽的题 corner case 这么多赛时从 WA30 搞到 WA50 再搞到 WA83 然后再也过不去了。最后发现是赛时没考虑局部最低点被卡掉了坏坏坏
考虑对于水平的平台,将排水口开在逆时针的最后一个点上。
一个显然的想法是考虑枚举每个点 \(i\) 作为排水口是否合法,发现若合法当且仅当:
- \(y_i - y_{i - 1} \le 0\);
- \(y_{i + 1} - y_{i} >0\);
- 保证该点对应的排水口在下方而非在天花板上,于是保证叉乘 \(\overrightarrow{P_{i-1}P_{i}}\times \overrightarrow{P_{i}P_{i+1}}\ge 0\);
- 保证该点是一个局部最低点,即考虑从该点开始分别逆时针顺时针枚举点,保证经过的点纵坐标不降情况下,都能找到一个纵坐标大于 \(y_i\) 的点。
满足上述条件的点即为合法的排水口。
大力枚举即可,总时间复杂度 \(O(n^2)\) 级别。
#include <bits/stdc++.h>
using namespace std;
#define int long long
const int N=1e6+5;
int T,n;
int x[N],y[N];
int fl[N];
int cha(int a,int b,int c,int d){
return a*d-b*c;
}
signed main(){
cin>>n;
for(int i=1;i<=n;++i){
cin>>x[i]>>y[i];
}
x[0] = x[n], y[0] = y[n];
x[n+1]=x[1],y[n+1]=y[1];
x[n+2]=x[2],y[n+2]=y[2];
int ans=0;
for(int i=1;i<=n;++i){
bool yes1 = 0, yes2 = 0;
for (int j = (i == n ? 1 : i + 1); j != i; j = (j == n ? 1 : j + 1)) {
yes1 |= y[j] > y[i];
if (y[j] < y[i]) break;
}
for (int j = (i == 1 ? n : i - 1); j != i; j = (j == 1 ? n : j - 1)) {
yes2 |= y[j] > y[i];
if (y[j] < y[i]) break;
}
if (!yes1 || !yes2) continue;
int a=x[i]-x[i - 1],b=y[i]-y[i - 1];
int c=x[i+1]-x[i],d=y[i+1]-y[i];
if(b<=0 && d>0&&cha(a,b,c,d) >= 0){
fl[i]=1;
}
}
for(int i=1;i<=n;++i){
if(fl[i]) ++ans;}
cout<<ans<<'\n';
return 0;
}
/*
6
0 0
1 1
2 1
3 0
3 2
0 2
*/
E 虚树,DP
J 二分图,贪心,构造
写在最后
唉唉 wenqizhi 大爹太强了感觉我是纯飞物。
学到了什么:
- A:使用非常人类智慧的表达式,尝试简化特判——并非好事!
- B:带修 DP,考虑不被修改的部分的决策能否尽可能地复用;
- M:sb corner case 啊