The 2024 ICPC Asia EC Regionals Online Contest (II)
写在前面
补题地址:https://codeforces.com/gym/105358。
以下按个人向难度排序。
妈的 7 题秒完剩下的题感觉没一个能做的。
F 签到
wenqizhi 大神秒了,我看都没看。
Code by wenqizhi:
#include <bits/stdc++.h>
#define ll long long
const int N = 1e5 + 5;
ll a[N], n;
int main() {
scanf("%d", &n);
ll ans = 1500;
for(int i = 1; i <= n; ++i)
{
scanf(" %d", &a[i]);
ans += a[i];
if(ans >= 4000)
{
printf("%d\n", i);
return 0;
}
}
printf("-1\n");
return 0;
}
A 枚举
显然每个队一定会选学校限制报名队伍数量最少的站,若该站的上限为 \(c\),则仅需考虑每个学校前 \(c\) 强的队伍即可。
然后将这些队伍排序,再枚举所有队伍检查他们单独插入后的排名即可,可使用 set
简单维护。
#include <bits/stdc++.h>
using namespace std;
#define ll long long
#define ull unsigned long long
#define pr pair
#define mp make_pair
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 kInf = 1e9 + 10;
const int kN = 1e5 + 10;
int num, n, k;
int w[kN], schoolid[kN];
vector<pr <int, int> > team[kN];
map <string, int> id;
set <pr <int, int> > endteam;
int ans[kN];
int main() {
ios::sync_with_stdio(0);
cin.tie(0), cout.tie(0);
cin >> n >> k;
int minc = kInf;
for (int i = 1; i <= k; ++ i) {
int c; cin >> c;
minc = min(minc, c);
}
for (int i = 1; i <= n; ++i) {
string school; int x;
cin >> x >> school;
if (!id.count(school)) id[school] = ++ num;
team[id[school]].push_back(mp(-x, i));
w[i] = x;
schoolid[i] = id[school];
}
for (int i = 1; i <= num; ++ i) {
sort(team[i].begin(), team[i].end());
while (team[i].size() > minc) team[i].pop_back();
for (auto x: team[i]) endteam.insert(x);
}
int cnt = 0;
endteam.insert(mp(-1, n + 1));
for (auto x: endteam) {
// cout << x.first << " " << x.second << "\n";
ans[x.second] = ++ cnt;
}
for (int i = 1; i <= n; ++ i) {
if (ans[i]) continue;
auto it = endteam.lower_bound(mp(-w[i], 0));
ans[i] = ans[it->second] - 1;
}
for (int i = 1; i <= n; ++ i) cout << ans[i] << "\n";
return 0;
}
J 贪心
保证了 \(c\le \frac{v}{\sum w}\),则实际上不需要考虑 \(v\) 的值,仅需最大化 \(\sum_i c_i\times W\) 即可。
发现相邻两个位置交换后仅影响这两个位置的 \(c_i\times W\),对前后缀每个位置的贡献无影响,一眼国王游戏,考虑微扰法发现仅需按照 \(\frac{c}{w}\) 升序排序即可。
code by wenqizhi:
#include <bits/stdc++.h>
using namespace std;
#define ll long long
#define ull unsigned long long
ll read()
{
ll 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 = 1e5 + 5;
int n;
struct node
{
ll w, v, c;
node(){ w = v = c = 0; }
bool friend operator < (const node &a, const node &b)
{ return a.c * b.w < b.c * a.w ; }
}A[N];
int main() {
// ios::sync_with_stdio(0);
// cin.tie(0), cout.tie(0);
n = read();
ll V = 0, W = 0;
for(int i = 1; i <= n; ++i)
{
A[i].w = read(), A[i].v = read(), A[i].c = read();
V += A[i].v ;
}
sort(A + 1, A + n + 1);
for(int i = 1; i <= n; ++i) V -= W * A[i].c , W += A[i].w ;
printf("%lld\n", V);
return 0;
}
I 构造,二进制
发现对于第 \(i\) 位上 1,可以将其调整为 \(2^i = 2^{i + 1} - 2^{i}\) 的形式,即变为二进制位上的一个 1 一个 -1,则可将第 \(i+1\) 位上的 0 消去。
然后发现不断通过上述策略调整即可仅使用 1 和 -1 对原有的二进制位进行调整,消去原有的 0 进行构造。若无法构造说明无解,且发现此时一定是 4 的倍数,使得无论怎么构造最后两位的 0 都无法消去。
code by dztlb:
#include <bits/stdc++.h>
using namespace std;
#define int long long
const int N=1e6+5;
#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;
}
int n,T;
int a[33];
signed main() {
ios::sync_with_stdio(0);
cin.tie(0), cout.tie(0);
// n=read();
std::cin>>T;
while(T--){
cin>>n;
for(int i=0;i<=31;++i){
if((n>>i)&1) a[i]=1;
else a[i]=0;
}
for(int i=0;i<31;++i){
if(a[i]==1&&a[i+1]==0) a[i+1]=1,a[i]=-1;
}
bool fl=0;
for(int i=0;i<31;++i){
if(a[i]==0&&a[i+1]==0) fl=1;
}
if(fl) cout<<"NO\n";
else{
cout<<"YES\n";
// cout<<a[31]<<endl;
for(int i=1;i<=4;++i){
for(int j=0;j<8;++j){
cout<<a[(i-1)*8+j]<<' ';
}
cout<<'\n';
}
}
}
return 0;
}
L 数学,三分
dztlb 大神秒了我看都没看,上去给大神写了个整数三分就过了。
显然两种操作不会交替使用,因为先进行操作 1 后进行操作 2,发现本次操作 1 实际上是没有贡献的。则一定是先不断进行操作 2 直至某个阈值 \(c\),再不断进行操作 1 直至 0。
于是考虑枚举进行若干次操作 2 后的阈值 \(c\),求得第一次操作使得小于该阈值的期望步数。一次操作的概率为 \(p = \frac{c}{t}\),发现这是个典型的 \(r=1\) 的帕斯卡分布,仅需考虑不小于 \(k(k\ge 0)\) 次操作后使大于阈值的概率,考虑级数求和可知期望步数即:
然后考虑操作 2 使得小于阈值后的取值在 \(0\sim c\) 中概率均等,则进行操作 1 的期望次数显然即:
则对于某个阈值 \(c\) 的期望操作次数即为:
显然是个对勾函数的形式,可以直接解出或三分求得极值点。
code by dztlb:
#include <bits/stdc++.h>
using namespace std;
#define int long long
const int N=1e6+5;
#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;
}
int n;
int t;
double check(int k){
return (double)(k-1.00)/2+(double)t/k;
}
signed main() {
ios::sync_with_stdio(0);
cin.tie(0), cout.tie(0);
// n=read();
std::cin>>n;
for(int i=1;i<=n;++i){
cin>>t;
// for(int j=1;j<=t;++j){
// cout<<j<<' '<<check(j)<<'\n';
// }
int lmid, rmid;
int id=1;
double lans, rans,ans=check(1);
for (int l = 1, r = t; l <= r; ) {
lmid = l + (r - l + 1) / 3;
rmid = r - (r - l + 1) / 3;
lans = check(lmid);
rans = check(rmid);
if(ans>lans) id=lmid,ans=lans;
if(ans>rans) id=rmid,ans=rans;
// cout<<lans<<' '<<rans<<' '<<l<<' '<<r<<'\n';
if (lans <= rans) r = rmid - 1;
else l = lmid + 1;
}
for(int j=min(lmid,rmid)-5;j<=max(lmid,rmid)+5;++j){
if(j<1||j>t) continue;
lans = check(j);
if(ans>lans) id=j,ans=lans;
}
// id=13;
int x=id*(id-1)+2*t;
int y=2*id;
int gg=__gcd(x,y);
x/=gg,y/=gg;
cout<<x<<' '<<y<<'\n';
}
return 0;
}
G 数学,辗转相除
发现薯薯的总数单调递减,于是想到能否递归地计算。
显然仅需分别考虑 \(x>y\) 与 \(x<y\) 的情况,然后可以分别写出两种情况的递推公式。发现每次递归均会使 \(x:=x-y\) 或 \(y:=y-x\) 是一个辗转相减的形式,且都可以转化为等比数列的形式,于是考虑将同种的连续的辗转相减合并为辗转相除,然后套用等比数列公式即可。
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 mod = 998244353;
ll qpow(ll a, ll b, ll mod)
{
ll ans = 1;
while(b)
{
if(b & 1) ans = ans * a % mod;
b >>= 1;
a = a * a % mod;
}
return ans;
}
ll a0, a1, b, p0, p1, p2;
ll dfs(ll x, ll y)
{
if(x == y) return p0 * p2 % mod;
if(x < y)
{
int k = y / x;
if(y % x == 0) --k;
return dfs(x, y - k * x) * qpow(p0 * p2 % mod, k, mod) % mod;
}else
{
int k = x / y;
if(x % y == 0) --k;
return ((dfs(x - k * y, y) - 1 + mod) % mod * qpow(p1 * p2 % mod, k, mod) % mod + 1) % mod;
}
}
void solve()
{
int x = read(), y = read();
a0 = read(), a1 = read(), b = read();
p0 = a0 * qpow(b, mod - 2, mod) % mod, p1 = a1 * qpow(b, mod - 2, mod) % mod, p2 = (1 - p0 - p1 + mod + mod) % mod;
p2 = qpow((1 - p2 + mod) % mod, mod - 2, mod);
printf("%lld\n", dfs(x, y));
}
int main() {
// ios::sync_with_stdio(0);
// cin.tie(0), cout.tie(0);
int T = read();
while(T--) solve();
return 0;
}
E 结论,最短路
dztlb 大神秒了,我看都没看。
看起来就是简单题详见题解吧。
code by dztlb:
#include <bits/stdc++.h>
using namespace std;
#define int long long
const int N=2e5+5;
const int M=2e6+5;
const int inf=1e18;
#define ll long long
#define ull unsigned long long
int T,n,m,d,k;
int id[N][2];
int s[N];
int lim[N][2],tot,head[N*2];
int f[N][2],pre[N][2];
int p[N*4],L,R;
struct node{
int to,nxt;
}e[M<<1];
void add(int u,int v){
e[++tot].to=v,e[tot].nxt=head[u],head[u]=tot;
}
void get_new(bool fl,int now,int u,int v){
// if(u==3){
// cout<<"!!\n";
// cout<<fl<<' '<<now<<' '<<u<<' '<<v<<endl;
// }
if(lim[u][fl]>now&&f[u][fl]==inf){
// cout<<u<<endl;
f[u][fl]=now;
pre[u][fl]=v;
if(fl) p[++R]=u+n;
else p[++R]=u;
}
}
int ans[N*4][2],la[2];
signed main(){
ios::sync_with_stdio(0);
cin.tie(0), cout.tie(0);
cin>>T;
while(T--){
cin>>n>>m>>d;
for(int i=1;i<=n;++i) id[i][0]=i,id[i][1]=n+i;
for(int i=0;i<=n;++i){
f[i][0]=f[i][1]=lim[i][0]=lim[i][1]=inf;
pre[i][0]=0;
pre[i][1]=0;
}
tot=0;
for(int i=1;i<=2*n;++i) head[i]=0;
for(int i=1,u,v;i<=m;++i){
cin>>u>>v;
add(u,v),add(v,u);
// add(id[u][0],id[v][1]);
// add(id[u][1],id[v][0]);
// add(id[v][1],id[u][0]);
// add(id[v][0],id[u][1]);
}
cin>>k;
for(int i=1;i<=k;++i){
cin>>s[i];
p[i]=id[s[i]][0];
lim[s[i]][0]=0;
}
L=1,R=k;
while(L<=R){
int u=p[L];
++L;
bool fl=0;
if(u>n) fl=1,u-=n;
// cout<<u<<' '<<fl<<'\n';
int now=lim[u][fl];
if(now>=d) continue;
for(int i=head[u];i;i=e[i].nxt){
int v=e[i].to;
// cout<<v<<"!!!\n";
// cout<<lim[v][!fl]<<endl;
if(lim[v][!fl]==inf){
lim[v][!fl]=now+1;
p[++R]=id[v][!fl];
}
}
}
// lim done
// for(int i=1;i<=n;++i){
// cout<<i<<' '<<lim[i][0]<<' '<<lim[i][1]<<'\n';
// }
L=1,R=0;
get_new(0,0,1,0);
// cout<<f[1][0]<<' '<<f[1][1]<<"askljdsjlak"<<endl;
while(R>=L){
int u=p[L];
++L;
bool fl=0;
if(u>n) fl=1,u-=n;
// cout<<u<<' '<<fl<<"!!!\n";
for(int i=head[u];i;i=e[i].nxt){
int v=e[i].to;
// cout<<v<<"???????\n";
int now=f[u][fl]+1;
get_new(!fl,now,v,u);
}
}
la[0]=la[1]=inf;
for(int ty=0;ty<=1;++ty){
int tmp=ty,now=n;
if(f[n][ty]!=inf){
la[ty]=1;
while(now){
ans[la[ty]][ty]=now;
now=pre[now][tmp];
++la[ty];
tmp=!tmp;
}
}
}
if(la[0]==inf&&la[1]==inf){
cout<<-1<<'\n';
continue;
}
if(la[0]>la[1]){
cout<<la[1]-2<<'\n';
for(int i=la[1]-1;i>=1;--i){
cout<<ans[i][1]<<' ';
} cout<<'\n';
}else{
cout<<la[0]-2<<'\n';
for(int i=la[0]-1;i>=1;--i){
cout<<ans[i][0]<<' ';
} cout<<'\n';
}
}
return 0;
}
/*
1
7 8 2
1 2
2 3
3 7
2 5
5 6
3 6
1 4
4 5
1 4
*/
C 字符串,kmp,结论
场上看到字符串就高超了在这题坐牢 2h 呃呃
以下为官方做法基础上的优化版本。
发现贡献的形式实际上即 Z 函数的定义。枚举每一个后缀 \(i\),求它与整个串的最长公共前缀 \([i, i+z_i-1]\),则其对答案的贡献即:\(B_{i}\times \sum_{j=i}^{i+z_i - 1} A_j\)。
考虑每次添加字符 \(S_i\) 后计算答案的增量。发现添加字符后,对 \(z_1\sim z_i\) 的影响有如下四种情况:
- 若 \(j+z_j - 1 < i- 1\),则 \(z_j\) 已确定,之后的添加操作均无影响;
- 若 \(j+z_j - 1 = i - 1\),且 \(S_i \not= S_{z_j + 1}\),则会令 \(z_j\) 确定,之后的添加操作均无影响;
- 若 \(j+z_j - 1 = i - 1\),且 \(S_i = S_{z_j + 1}\),则 \(z_j:= z_j + 1\);
- 特判是否有 \(S_1 = S_i\),若有则 \(z_i = 1\)。
则本次答案的增量,即上述情况 3、4 的 \(B_j\) 之和乘 \(A_i\)。于是考虑维护有贡献的 \(B_j\) 之和,仅需每次在确定 \(z_j\) 的同时,减去情况 2 的 \(B_j\) 的贡献,再加上情况 4 的 \(B_j\) 的贡献即可。
然而强制在线显然不能直接跑 Z 函数,但是 KMP 支持末尾添加字符,于是考虑如何使用 KMP 实现上述功能。发现若满足 \(j+z_j - 1 = i - 1\),则 \(S[1:z_j]\) 一定是 \(i-1\) 的 border,于是仅需考虑枚举 \(i-1\) 的 border,并考虑下一个字符是否为 \(S_i\) 即可完成贡献的 \(B_j\) 之和的修改。
然而并不能直接暴跳 border 太呃呃了,考虑按照 border 的下一个字符的种类路径压缩一下,记 \(\operatorname{fa}_i\) 表示前缀 \(s[1:i]\) 的满足 \(s_{k+1}\not= s_{i+1}\) 的第一个 border \(s[1:p]\),于是每次仅需从 \(i-1\) 不断跳 \(\operatorname{fa}\) 和 \(\operatorname{fail}\),并满足将 \(s_{p+1}\not= s_{i+1}\) 的 border 的贡献 \(B_{i-p}\) 减去即可。
可以保证每个 \(B_i\) 仅会在贡献中被加一次减一次,由字符串的性质可知所有跳 \(\operatorname{fa}\) 的次数加起来是线性级别的,则暴跳复杂度是正确的。
实现详见代码,总时间复杂度 \(O(n)\) 级别。
#include <bits/stdc++.h>
#define LL long long
const int kN = 5e5 + 10;
int n;
int S[kN], A[kN], B[kN];
int fail[kN], fa[kN], vis[kN];
void solve() {
LL ans = 0, sumb = 0;
fail[1] = 0;
for (int i = 1, j = 0; i <= n; ++ i) {
std::cin >> S[i] >> A[i] >> B[i];
S[i] = (1ll * ans + S[i]) % n;
if (i > 1) {
fa[i - 1] = S[fail[i - 1] + 1] == S[i] ? fa[fail[i - 1]] : fail[i - 1];
while (j && S[i] != S[j + 1]) j = fail[j];
if (S[j + 1] == S[i]) ++ j;
fail[i] = j;
}
if (S[i] == S[1]) sumb += B[i];
int p = i - 1;
while (p) {
int q = fa[p];
if (S[p + 1] != S[i]) {
while (p != q) sumb -= B[i - p], p = fail[p];
}
p = q;
}
ans += 1ll * sumb * A[i];
std::cout << ans << "\n";
}
}
int main() {
// freopen("1.txt", "r", stdin);
std::ios::sync_with_stdio(0);
std::cin.tie(0), std::cout.tie(0);
std::cin >> n;
solve();
return 0;
}
/*
6
0 1 1
1 2 2
0 3 3
0 4 4
1 5 5
0 6 6
*/
写在最后
感觉这场一直在摸鱼呃呃队友太强了都。
学到了什么:
- L:期望的线性性。
- G:辗转相减 \(\iff\) 辗转相除;