2023 ICPC 横滨区域赛题解 更新至 7 题 (2023-2024 ICPC, Asia Yokohama Regional Contest 2023)
Preface
是谁开场写个签到题都差点红温了,我不说。
好久没打区域赛了,v的这场难度其实比正常的区域赛要简单一点点,但是后面的题阿巴该死的还是屁都不会做啊。
看了2小时的E题,只能说确实比较有难度,属于那种干坐着想不出来,看了题解恍然大悟的程度。
G题也没有怎么看,其实G比E要简单一些,但是当时被E题给破防了,G题知道了题意但是毫无战意。
其他的题倒是还好。思维上我是个弱智。
我会在代码一些有必要的地方加上注释,签到题可能一般就不会写了.
以下是代码火车头:
#include <iostream>
#include <algorithm>
#include <cstring>
#include <cmath>
#include <vector>
#include <set>
#include <queue>
#include <map>
#include <unordered_map>
#include <iomanip>
#define endl '\n'
#define int long long
#define rep(i,a,b) for(int i=(a);i<=(b);i++)
#define rep2(i,a,b) for(int i=(a);i>=(b);i--)
using namespace std;
template<typename T>
void cc(const vector<T> &tem) {
for (const auto &x: tem) cout << x << ' ';
cout << endl;
}
template<typename T>
void cc(const T &a) { cout << a << endl; }
template<typename T1, typename T2>
void cc(const T1 &a, const T2 &b) { cout << a << ' ' << b << endl; }
template<typename T1, typename T2, typename T3>
void cc(const T1 &a, const T2 &b, const T3 &c) { cout << a << ' ' << b << ' ' << c << endl; }
void cc(const string &s) { cout << s << endl; }
void fileRead() {
#ifdef LOCALL
freopen("D:\\AADVISE\\Clioncode\\untitled2\\in.txt", "r", stdin);
freopen("D:\\AADVISE\\Clioncode\\untitled2\\out.txt", "w", stdout);
#endif
}
void kuaidu() { ios::sync_with_stdio(false), cin.tie(0), cout.tie(0); }
inline int max(int a, int b) {
if (a < b) return b;
return a;
}
inline double max(double a, double b) {
if (a < b) return b;
return a;
}
inline int min(int a, int b) {
if (a < b) return a;
return b;
}
inline double min(double a, double b) {
if (a < b) return a;
return b;
}
void cmax(int &a, const int &b) { if (b > a) a = b; }
void cmin(int &a, const int &b) { if (b < a) a = b; }
void cmin(double &a, const double &b) { if (b < a) a = b; }
void cmax(double &a, const double &b) { if (b > a) a = b; }
using PII = pair<int, int>;
using i128 = __int128;
using vec_int = std::vector<int>;
using vec_char = std::vector<char>;
using vec_double = std::vector<double>;
using vec_int2 = std::vector<std::vector<int> >;
using que_int = std::queue<int>;
Problem A. Yokohama Phenomena
哥们,不会有人dfs不会吧
签到题写一个easy的dfs就好了,不过签到就写dfs的倒是挺少见的。
//--------------------------------------------------------------------------------
const int N = 1e5 + 10;
const int M = 1e6 + 10;
const int mod = 1e9 + 7;
const int INF = 1e16;
int n, m, T;
char A[11][11];
string to = "YOKOHAMA";
int Q[4] = { 1,0,-1,0 };
int W[4] = { 0,1,0,-1 };
int ans;
//--------------------------------------------------------------------------------
//struct or namespace:
//--------------------------------------------------------------------------------
void dfs(int x, int y, int step) {
if (step == 8) {
ans++;
return;
}
rep(k, 0, 3) {
int tx = x + Q[k], ty = y + W[k];
if (tx<1 or ty<1 or tx>n or ty>m) continue;
if (A[tx][ty] != to[step]) continue;
dfs(tx, ty, step + 1);
}
}
signed main() {
fileRead();
kuaidu();
T = 1;
//cin >> T;
while (T--) {
cin >> n >> m;
rep(i, 1, n) {
rep(j, 1, m) {
cin >> A[i][j];
}
}
rep(i, 1, n) {
rep(j, 1, m) {
if (A[i][j] == to[0]) {
dfs(i, j, 1);
}
}
}
cc(ans);
}
return 0;
}
Problem B. Rank Promotion
先设置来\(pre_i\)是前\(i\)个里面Y的个数,那么我们如果希望第i项是可以升级的,那么会有。
j的范围可以推断出来在一个区间里面,然后这个式子我们变化一下:(这里a是\(p/q\),我懒得写了)
我们可以用存一下这个的形式的最小值,然后只要有存在比它大的,就代表我们可以升级。
貌似其实有更简单的实现办法,可能当时想复杂了。
代码是队友实现的:
#include<bits/stdc++.h>
#define endl '\n'
#define int long long
using namespace std;
const int N=500005;
const int INF=1e18;
int n,c,p,q;
int rk;
int lst;
int ny;
string str;
int pre[N];
int mnp;
int val(int x){
return pre[x]*q-p*x;
}
void solve(){
cin>>n>>c>>p>>q>>str;
str='*'+str;
for(int i=1;i<=n;++i)
pre[i]=pre[i-1]+(str[i]=='Y');
lst=mnp=0;
for(int i=1;i<=n;++i){
if(i-lst<c)continue;
//insert i-c
if(val(i-c)<val(mnp))
mnp=i-c;
if(val(i)>=val(mnp)){
rk++;
lst=i;
mnp=i;
}
}
cout<<rk<<endl;
return;
}
signed main(void){
ios::sync_with_stdio(false),cin.tie(0);
//int T; cin>>T; while(T--)
solve();
return 0;
}
Problem D. Nested Repetition Compression
非常典的区间dp啊。
我们直接设\(dp_{l,r}\)是区间从l到r之间可以构造出来的最短的字符串,那么我们转移有两种办法,要么是直接枚举中间的断点mid,要么就是当前这个区间可以直接进行缩写,也就是说我们直接枚举倍数就好了,前者的复杂度是n,后者的复杂度肯定也不会超过n,所以总体的复杂度就是n的3次方那里。
然后我们判断可否可以分割成k个相同的字符串这里我写了个哈希。
//--------------------------------------------------------------------------------
const int N = 2e2 + 10;
const int M = 1e6 + 10;
const int mod = 1e9 + 7;
const int INF = 1e16;
int n, m, T;
//--------------------------------------------------------------------------------
//struct or namespace:
class HASH {
int base = 131;
int P[2][N], pre[2][N];
char S[N];
int Mod[2];
int n;
bool REVERSE;
public:
void clear(const string& s, bool fl = 0) {
Mod[0] = 998244353;
Mod[1] = 1e9 + 7;
n = s.size(), REVERSE = fl;
if (REVERSE)
for (int i = 1; i <= n; i++) S[i] = s[n - i];
else
for (int i = 1; i <= n; i++) S[i] = s[i - 1];
P[0][0] = P[1][0] = 1;
pre[0][0] = 0;
for (int i = 0; i <= 1; i++) {
for (int j = 1; j <= n; j++) {
pre[i][j] = (pre[i][j - 1] * base % Mod[i] + S[j]) % Mod[i];
P[i][j] = P[i][j - 1] * base % Mod[i];
}
}
}
PII qry(int l, int r) {
int a, b;
l++, r++;
if (REVERSE) l = n + 1 - l, r = n + 1 - r, swap(l, r);
a = (pre[0][r] - pre[0][l - 1] * P[0][r - l + 1] % Mod[0] + Mod[0]) % Mod[0];
b = (pre[1][r] - pre[1][l - 1] * P[1][r - l + 1] % Mod[1] + Mod[1]) % Mod[1];
return { a, b };
}
};
HASH hs; string s;
//--------------------------------------------------------------------------------
bool fl[N][N];
string dp[N][N];
string dfs(int l, int r) {
if ((l == r) or (l + 1 == r)) {
string tem = s.substr(l, r - l + 1);
return tem;
}
if (fl[l][r]) {
return dp[l][r];
}
fl[l][r] = 1;
string tem = s.substr(l, r - l + 1);
string mmin = "";
rep(mid, l, r - 1) {
if (mmin == "") mmin = dfs(l, mid) + dfs(mid + 1, r);
else {
string tem2 = dfs(l, mid) + dfs(mid + 1, r);
if (tem2.size() < mmin.size()) mmin = std::move(tem2);
}
}
if (mmin.size() < tem.size()) tem = mmin;
rep(k, 2, r - l + 1) {
if ((r - l + 1) % k != 0) continue;
int len = (r - l + 1) / k;
if (k > 9) break;
PII haxi = hs.qry(l, l + len - 1);
bool flag = 1;
for (int i = l + len - 1; i <= r; i += len) {
PII haxi2 = hs.qry(i - len + 1, i);
if (haxi2 != haxi) {
flag = 0;
break;
}
}
if (flag == 1) {
string s = to_string(k) + '(' + dfs(l, l + len - 1) + ')';
if (s.size() < tem.size()) tem = s;
}
}
dp[l][r] = tem;
return tem;
}
signed main() {
fileRead();
kuaidu();
T = 1;
//cin >> T;
while (T--) {
cin >> s;
hs.clear(s);
n = s.size();
cc(dfs(0, n - 1));
}
return 0;
}
Problem E. Chayas
md看了好久但是还是不会做的题。哎,还是太菜了。
能想得到对于每一个中间的点b我们可以看做是一个图,把a和c连上边,其实他们是构成像二分图一样(实际不是)的东西,然后再写写画画我们可以知道,每一个相邻之间的他们都一定是属于不同的地方,例如如果1--3--5--2,那么1和3肯定是在一侧,5和2就一定是在相对的另一侧,也就是说我们用dfs跑出来他们,给他们染上色。
无解肯定就是染色失败的情况。
有个细节的地方是:这个图不一定是联通的,会有多个联通块,那在一侧的结果就会是这些多个联通块的他们的组合,每一个联通块的颜色染成0的部分称为\(s_{i,0}\),染成1的称为\(s_{i,1}\),那么\(s_{i,1}\)可以和\(s_{j,0}\)或者是\(s_{j,1}\)去组合,这里我们也可以直接dfs2递归处理。
之后就是没有想到的地方了,我们可以用状压dp去做,当前状态称之为S,代表我们已经放进去的点集,那么我们如何判断当前一个点i可不可以放进去,只需要知道这个点放进去的时候可以允许的点集,我们刚才dfs2其实就是可以处理当前一个点放进去的时候可以允许的点集了,所以就直接跑一个状态转移就好了。
小细节是:我们要转移的时候,要保证状态S1的1的个数是大于状态S2的1的个数的,也就是我们要从1的个数少的开始枚举,逐渐多起来。
//--------------------------------------------------------------------------------
const int N = 24 + 10;
const int M = 16777216 + 10;
// const int M = 10000;
const int mod = 998244353;
// const int INF = 1e16;
int n, m, T;
//--------------------------------------------------------------------------------
//global variable:
//struct or namespace:
//--------------------------------------------------------------------------------
vec<int> A[N][N];
bitset<N> co;
int id[N];
int idx;
bitset<M> flag[N];
// bool flag[M][N];
int dp[M];
int cnt[2][N];
vec<int> ge[N];
bool dfs(int rt, int x, int cur) {
// cc(rt, x, cur);
id[x] = cur;
for (auto y : A[rt][x]) {
if (id[y] > 0) {
if (co[y] != (co[x] ^ 1)) return false;
continue;
}
co[y] = (co[x] ^ 1);
if (!dfs(rt, y, cur)) return false;
}
return true;
}
void dfs2(int rt, int cur, int S) {
if (cur >= idx + 1) {
flag[rt][S] = 1;
// cc(rt, S);
return;
}
dfs2(rt, cur + 1, S | (cnt[0][cur]));
dfs2(rt, cur + 1, S | (cnt[1][cur]));
}
bool work(int x) {
idx = 0;
rep(i, 1, n) {
id[i] = 0, co[i] = 0;
cnt[0][i] = cnt[1][i] = 0;
}
//memset(cnt[0], 0, sizeof(cnt[0]));
//memset(cnt[1], 0, sizeof(cnt[1]));
rep(i, 1, n) {
if (i == x) continue;
if (id[i] > 0) continue;
co[i] = 0, ++idx;
if (!dfs(x, i, idx)) return false;
}
rep(i, 1, n) {
if (i == x) continue;
cnt[co[i] == 1][id[i]] |= (1 << (i - 1));
}
dfs2(x, 1, 0);
return true;
}
signed main() {
// fileRead();
kuaidu();
T = 1;
//cin >> T;
while (T--) {
cin >> n >> m;
rep(i, 1, m) {
int a, b, c;
cin >> a >> b >> c;
A[b][a].push_back(c);
A[b][c].push_back(a);
}
// cc(222);
bool fl = 1;
rep(i, 1, n) {
if (work(i) == 0) {
fl = 0;
break;
}
}
if (fl == 0) {
cc(0);
continue;
}
int lim = 1 << n;
lim -= 1;
// cc(111);
rep(i, 0, n) ge[i].clear();
rep(i, 0, lim) {
ge[__popcount(i)].push_back(i);
}
dp[0] = 1;
rep(i, 0, n) {
if (ge[i].empty()) continue;
for (auto S : ge[i]) {
rep(j, 1, n) {
// cc("--");
if (flag[j][S] == 0) continue;
int y = S | (1ll << (j - 1));
dp[y] = (long long)(dp[y] + dp[S]) % mod;
// dp[y] += dp[x];
}
}
}
cc(dp[lim]);
// cc("END");
}
return 0;
}
/*
*/
Problem F. Color Inversion on a Huge Chessboard
狗屎题,我吃屎了兄弟们。
一个简单的分类讨论给我写成了屎一样的东西。
这个问题我们可以考虑成,假设一开始矩阵全是0,可以行或者列操作,最后会有多少的联通块。
分开考虑行和列,分别维护数组,0代表没操作,1代表操作了。最后数组的联通块的乘积就是答案。
如何变成问题开始的那样呢?我们只需要对行0,1,0,1这样的操作就可以了,列也是这样。
代码不想改了,又臭又长。
//--------------------------------------------------------------------------------
const int N = 5e5 + 10;
const int M = 1e6 + 10;
const int mod = 1e9 + 7;
const int INF = 1e16;
int n, m, T;
int row[N], col[N];
int rl, cl;
//--------------------------------------------------------------------------------
//struct or namespace:
//100000000
//--------------------------------------------------------------------------------
// unordered_map<string, int> mp;
signed main() {
fileRead();
kuaidu();
T = 1;
//cin >> T;
while (T--) {
cin >> n >> m;
if (n == 1) {
rep(i, 1, m) {
string s; cin >> s;
int id; cin >> id;
cout << 1 << endl;
}
continue;
}
int ans = 0;
rl = n, cl = n;
rep(i, 1, n) {
row[i] = i % 2, col[i] = i % 2;
}
rep(i, 1, m) {
string s; cin >> s;
int id; cin >> id;
if (s == "ROW") {
int newrl = rl, newcl = cl;
if (id == 1 or id == n) {
if (id == 1) {
if (row[id + 1] == row[id]) newrl += 1;
else newrl -= 1;
}
else
if (id == n) {
if (row[id - 1] == row[id]) newrl += 1;
else {
newrl -= 1;
}
}
row[id] ^= 1;
ans = newrl * newcl;
rl = newrl, cl = newcl;
cc(ans);
continue;
}
if (row[id - 1] == row[id] and row[id] == row[id + 1]) {
newrl += 2;
}
else if (row[id - 1] == row[id + 1] and row[id] != row[id + 1]) {
newrl -= 2;
}
row[id] ^= 1;
ans = newrl * newcl;
cc(ans);
rl = newrl, cl = newcl;
}
else {
int newrl = rl, newcl = cl;
if (id == 1 or id == n) {
if (id == 1) {
if (col[id + 1] == col[id]) newcl += 1;
else newcl -= 1;
}
else
if (id == n) {
if (col[id - 1] == col[id]) newcl += 1;
else {
newcl -= 1;
}
}
col[id] ^= 1;
ans = newrl * newcl;
rl = newrl, cl = newcl;
cc(ans);
continue;
}
if (col[id - 1] == col[id] and col[id] == col[id + 1]) {
newcl += 2;
}
else if (col[id - 1] == col[id + 1] and col[id] != col[id + 1]) {
newcl -= 2;
}
col[id] ^= 1;
ans = newrl * newcl;
cc(ans);
rl = newrl, cl = newcl;
}
}
}
return 0;
}
Problem G. Fortune Telling
一个看似比较困难,但是其实还好的难度题,估计难度应该是铜或者铜-。
难点其实在于写dp。
设计\(dp_{n,k}\),代表当前有\(n\)个数,第\(k\)个数字的存活概率是多少。
那么我们的一次操作会带来的影响就是会减少大概\((n/6)\)个数字,所以我们最多操作的次数其实就没有多少次。开一个unordered_map比较合适。
假设扔筛子的点数是i,那么如果\((n%6)\)>\(=i\),就代表是减少了\(n/6+1\)个数字,否则就是减少了\(n/6\)个数字。\(k\)也是这样的类似的考虑。
来一个记忆化搜索。
//--------------------------------------------------------------------------------
const int N = 3e5 + 10;
const int M = 1e6 + 10;
const int mod = 998244353;
const int INF = 1e16;
int n, m, T;
//--------------------------------------------------------------------------------
//struct or namespace:
//--------------------------------------------------------------------------------
unordered_map<int, int> dp[N];
int fenmu[7];
int dfs(int n, int k) {
if (n == 1 and k == 0) return 1;
if (dp[n].count(k)) {
return dp[n][k];
}
dp[n][k] = 0;
rep(i, 0, min(6, n) - 1) {
if (k % 6 == i) continue;
int tn = n - n / 6, tk = k - k / 6;
if (n % 6 > i) tn--;
if (k % 6 > i) tk--;
dp[n][k] += dfs(tn, tk) * fenmu[min(6, n)] % mod;
dp[n][k] %= mod;
}
return dp[n][k];
}
signed main() {
fileRead();
kuaidu();
T = 1;
//cin >> T;
fenmu[0] = 1;
rep(i, 1, 6) fenmu[i] = Kuai<mod>(i, mod - 2);
while (T--) {
cin >> n;
rep(i, 0, n - 1) {
cc(dfs(n, i));
}
}
return 0;
}
/*
*/
Problem K. Probing the Disk
这个题也比较恶心,不能测试,大概只能自己感觉对了就交,直接变成IOI。
思路是我们一开始先横着横着查,间隔是199,这样我们就一定可以查出来这个圆的y坐标是多少。
查到了之后我们就可以根据当前的点进行二分,二分的间隔是是否有覆盖。这样好些一些,如果是二分的覆盖线段的长度,估计有点麻烦。
然后竖着也是这样做的,但是却WA了。估计是勉勉强强超过了1024,感觉二分的长度是直径的话,也该能过,看到有人是这样做过了的。
张神的思路:横着操作之后,y轴我们固定是在圆的中间的位置。我们就二分覆盖线段的长度是直径,这样操作次数非常的放心。
代码是队友张神写的。
#include<bits/stdc++.h>
#define endl '\n'
#define mid ((L+R)/2)
#define ll long long
using namespace std;
const int N=100000;
const double eps=1e-6;
int hit;
int radius;
int hh,vv;
bool query_h(int i){
cout<<"query 0 "<<i<<" 100000 "<<i<<endl;
cout.flush();
double x; cin>>x; return fabs(x)>=eps;
}
bool query_v(int i){
cout<<"query 0 "<<hh<<' '<<i<<' '<<hh<<endl;
cout.flush();
double x; cin>>x; return (x>=2*radius-eps);
}
int solve_h(){
for(int i=199;i<=N;i+=199){
if(query_h(i)){
hit=i;
break;
}
}
int L,R,up,down;
L=hit; R=N;
while(L<=R){
if(query_h(mid)){
up=mid;
L=mid+1;
}else{
R=mid-1;
}
}
L=0; R=hit;
while(L<=R){
if(query_h(mid)){
down=mid;
R=mid-1;
}else{
L=mid+1;
}
}
up++; down--;
radius=(up-down)/2;
return (up+down)/2;
}
int solve_v(){
int L,R,ans;
L=0; R=N;
while(L<=R){
if(query_v(mid)){
ans=mid;
R=mid-1;
}else{
L=mid+1;
}
}
return ans-radius;
}
void solve(){
hh=solve_h();
vv=solve_v();
cout<<"answer "<<vv<<' '<<hh<<' '<<radius<<endl;
cout.flush();
}
signed main(void){
//ios::sync_with_stdio(false),cin.tie(0);
//int T; cin>>T; while(T--)
solve();
return 0;
}
PostScript
这把打得感觉不是太妙。
队友少了一个,西安邀请赛要双开了,感觉非常的害怕。
于是害怕的开始耍。