ICPC2023香港站题解(A D E H I J)
本场金牌为超低罚时六题,稳拿金牌需要做出第七题。
但是我只会六题,这里是前六题的题解。
ICPC2023香港站
J:
签到但不是完全签到,需要讲。
首先每个位置只会走一次,所以让 \(a_i\) 加一的操作只会在第一次到达某个位置时连续施行。
让 \(a_i\) 加一再跳转需要花费一个时间,让 \(a_i\) 加二再跳转需要花费两个时间,你可以理解成先走到 \(i+a_i\) 的位置,再花费 1 的时间往后走一个位置,也可以花费 2 的时间往后走两个位置。所以我们把每一个位置往下一个位置连花费为1的边就好了。
但这个连边是需要一个前提的,我们得能够先走到 \(i\) 的位置,这些向后连的边才有意义。比如终点是1,但是 \(a_0=2\),你是不能一步到达的。
解决方案很简单,不要把0当初始点,而是把 \(a_0\) 当初始点,因为走到终点是至少需要跳这一步的。
#include <algorithm>
#include <iostream>
#include <cstring>
#include <cstdio>
#include <cmath>
#define FOR() ll le=e[u].size();for(ll i=0;i<le;i++)
#define QWQ cout<<"QwQ\n";
#define ll long long
#include <vector>
#include <queue>
#include <map>
using namespace std;
const ll N=501010;
const ll qwq=303030;
const ll inf=0x3f3f3f3f;
ll T;
ll n,X;
ll ans,a[N];
struct E{
ll to,we;
};
vector <E> e[N];
struct D{
ll id,di;
};
bool operator < (D A,D B) { return A.di > B.di; }
priority_queue <D> q;
ll dis[N];
inline ll read() {
ll sum = 0, ff = 1; char c = getchar();
while(c<'0' || c>'9') { if(c=='-') ff = -1; c = getchar(); }
while(c>='0'&&c<='9') { sum = sum * 10 + c - '0'; c = getchar(); }
return sum * ff;
}
inline void add(ll u,ll v,ll z) {
e[u].push_back({v,z});
}
void DIJ() {
memset(dis,0x3f,sizeof(dis));
q.push({a[0],0}); dis[a[0]] = 0;
while(!q.empty()) {
D now = q.top(); q.pop();
ll u = now.id;
if(dis[u]!=now.di) continue;
FOR() {
ll v = e[u][i].to, w = e[u][i].we;
if(dis[u] + w < dis[v]) {
dis[v] = dis[u] + w;
q.push({v,dis[v]});
}
}
}
}
int main() {
n = read(); X = read();
for(ll i=0;i<n;i++) a[i] = read(), add(i,(a[i]+i)%n,1);
for(ll i=0;i<n;i++) add(i,(i+1)%n,1);
DIJ();
cout<<dis[X]+1;
return 0;
}
A:
一道很像网络流的矩阵选数题。题面中的 NP-complete 把我队友给误导了,其实解法很简单。
和之前一道类似的矩阵构造题很像,先全部改成一致颜色,然后只需要考虑一个方向。
这题的解法也是类似,先把所有的1改成0,之后修改任意位置其实都是加一。为了同时满足行和列的要求,有一个贪心的选数方法,比较经典。
#include <algorithm>
#include <iostream>
#include <cstring>
#include <cstdio>
#include <cmath>
#define FOR() int le=e[u].size();for(int i=0;i<le;i++)
#define QWQ cout<<"QwQ\n";
#define ll long long
#include <vector>
#include <queue>
#include <map>
using namespace std;
const int N=501010;
const int qwq=303030;
const int inf=0x3f3f3f3f;
int T;
int n,m;
int a[4040][4040],b[4040][4040];
char s[N];
int hang[N];
struct E{
int id,zhi;
}lie[N];
inline bool cmp(E A,E B) { return A.zhi < B.zhi; }
inline int read() {
int sum = 0, ff = 1; char c = getchar();
while(c<'0' || c>'9') { if(c=='-') ff = -1; c = getchar(); }
while(c>='0'&&c<='9') { sum = sum * 10 + c - '0'; c = getchar(); }
return sum * ff;
}
int main() {
n = read();
for(int i=1;i<=n;i++) lie[i].id = i;
for(int i=1;i<=n;i++) {
scanf("%s",s+1);
for(int j=1;j<=n;j++) {
if(s[j]=='-') a[i][j] = -1;
else {
a[i][j] = 1;
hang[i]--;
lie[j].zhi--;
}
}
}
for(int i=1;i<=n;i++) hang[i] += read();
for(int i=1;i<=n;i++) lie[i].zhi += read();
for(int i=1;i<=n;i++) {
if(hang[i]>0) { cout<<"No"; return 0; }
sort(lie+1,lie+n+1,cmp);
for(int j=1;j<=(-hang[i]);j++) lie[j].zhi++, b[i][lie[j].id] = 1;
}
// bool ke = 1;
for(int i=1;i<=n;i++) if(lie[i].zhi!=0) { cout<<"No"; return 0; }
cout<<"Yes\n";
for(int i=1;i<=n;i++) {
for(int j=1;j<=n;j++) {
if(a[i][j]==1) {
if(b[i][j]) cout<<0;
else cout<<1;
}
else {
if(b[i][j]) cout<<1;
else cout<<0;
}
}
cout<<endl;
}
return 0;
}
I:
当你的重置能力CD好了之后,你有两个选择:一是等你的普通攻击CD,CD好了之后先攻击,再重置,然后立即攻击;二是马上重置,然后攻击。
其他的选择,你手玩一下会发现是愚昧的。
而这两个选择结果都是相同的,都会使得两个技能重新开始冷却(其实就和重新开始一个情况了)。
这两种选择,周期不一样,攻击次数不一样。
数据范围允许我们枚举第二种选择的数量,然后推算出第一种选择的数量,剩余部分一直平A。
#include <algorithm>
#include <iostream>
#include <cstring>
#include <cstdio>
#include <cmath>
#define FOR() ll le=e[u].size();for(ll i=0;i<le;i++)
#define QWQ cout<<"QwQ\n";
#define ll long long
#include <vector>
#include <queue>
#include <map>
using namespace std;
const ll N=501010;
const ll qwq=303030;
const ll inf=0x3f3f3f3f;
ll T;
ll n,m;
ll A,B;
inline ll read() {
ll sum = 0, ff = 1; char c = getchar();
while(c<'0' || c>'9') { if(c=='-') ff = -1; c = getchar(); }
while(c>='0'&&c<='9') { sum = sum * 10 + c - '0'; c = getchar(); }
return sum * ff;
}
int main() {
T = read();
while(T--) {
A = read(); B = read(); m = read();
ll k = B/A;
if(B%A==0) {
cout<<(m/A+m/B+2)*160<<"\n";
continue;
}
k++;
ll res = 0;
for(ll i=0;i<=m;i+=B) {
ll wo = k*(i/B);
ll sheng = m-i;
ll ans = wo + sheng/(k*A) + sheng/A + 2;
res = max(res,ans);
}
cout<<res*160<<"\n";
}
return 0;
}
D:
每个点入度上限为3,简单多了。
首先一个点不能连三个红边,也不能连三个蓝边,所以红边的连通块只能是链或环。
然后发现连成环也允许,一个红色环至少需要三个点,这些点无法通过蓝边连起来。
所以蓝色连通块和红色连通块都是链。
然后发现,链的长度还不能超过四,也就是说最多四个点,因为一个点想要成为红点的链中意味着它要连两个红边,只能连一个蓝边,它就是蓝边的链头或链尾,我们不允许三个链头链尾出现,所以红链只能有两个链中,链长度最多是四。
然后就是分类讨论,一个点,两个点,三个点,四个点。四个情况。
#include <algorithm>
#include <iostream>
#include <cstring>
#include <cstdio>
#include <cmath>
#define FOR() ll le=e[u].size();for(ll i=0;i<le;i++)
#define QWQ cout<<"QwQ\n";
#define ll long long
#include <vector>
#include <queue>
#include <map>
using namespace std;
const ll N=501010;
const ll qwq=303030;
const ll inf=0x3f3f3f3f;
ll T;
ll n,m;
ll ans;
vector <ll> e[N],d[N];
map <ll,ll> f,g;
ll si[N];
inline ll read() {
ll sum = 0, ff = 1; char c = getchar();
while(c<'0' || c>'9') { if(c=='-') ff = -1; c = getchar(); }
while(c>='0'&&c<='9') { sum = sum * 10 + c - '0'; c = getchar(); }
return sum * ff;
}
ll check(ll A,ll C) {
if(!A || !C) return 0;
return (f[A*n+C] && g[A*n+C]);
}
int main() {
ll x,y,z;
n = read(); m = read();
for(ll i=1;i<=m;i++) {
x = read(); y = read(); z = read();
if(z==1) {
e[x].push_back(y);
e[y].push_back(x);
f[x*n+y] = f[y*n+x] = 1;
}
else {
d[x].push_back(y);
d[y].push_back(x);
g[x*n+y] = g[y*n+x] = 1;
}
}
for(ll i=1;i<=n;i++) if(e[i].size()==3 || d[i].size()==3) si[i] = 1;
ans = n;
ll res = 0;
for(auto v : f) { if(g[v.first]) res++; }
ans += res / 2;
res = 0;
ll A, B, C, D;
for(ll i=1;i<=n;i++) {
if(si[i]) continue;
A = B = C = D = 0;
if(e[i].size()==2) A = e[i][0], B = e[i][1];
if(e[i].size()==1) A = e[i][0];
if(d[i].size()==2) C = d[i][0], D = d[i][1];
if(d[i].size()==1) C = d[i][0];
res += check(A,C) + check(B,C) + check(A,D) + check(B,D);
}
ans += res;
res = 0;
for(ll i=1;i<=n;i++) {
if(e[i].size()!=1) continue;
A = i; B = e[i][0];
if(e[B].size()!=2) continue;
if(e[B][0]==A) C = e[B][1];
else C = e[B][0];
if(e[C].size()!=2) continue;
if(e[C][0]==B) D = e[C][1];
else D = e[C][0];
if(e[D].size()!=1) continue;
if(g[A*n+B] && g[A*n+D] && g[C*n+D]) res++;
if(g[A*n+C] && g[A*n+D] && g[B*n+D]) res++;
}
ans += res / 2;
cout<<ans;
return 0;
}
H:
妙妙树上DP,我写的这个是一个很奇怪的做法。
首先要发现一点:虽然车是按从小到大一个一个寻找空位的,但我们不需要关心它们的具体顺序,只要是一个合法的方案,我们调换任意两辆车的顺序,依旧是合法的。
所以我们dp时就不需要考虑车的编号了,只需要考虑每个点上有几辆车。
最后的答案如何统计,假如第 \(i\) 个点停了 \(b_i\) 辆车,那么这样的方案对应到车辆编号的情况数就有 \(\frac{n!}{b_1!b_2!...b_n!}\) 种,也就是超排列。我们不能让最后的方案数直接乘以 \(n!\) 就是因为有些车是在同一个点的,直接乘以 \(n!\) 会重复计算这一部分。我们 dp 出来的方案数不好统计究竟有多少车在同一个点,但我们发现可以把 \(b_i!\) 分母的这部分先计入我们的dp,也就是说,只要选了一个大小为 \(b_i\) 的点(\(b_i\) 辆车在这个点),我们就让 dp 结果除以 \(b_i\),最后再乘上一个 \(n!\) 就是最终答案了。
dp 方程怎么设,怎么转移,需要观察题目的性质。
因为题目中的过程所有车都是根向走的,我们将时间倒流,每个点上有一辆车,所有车往叶向走回到初始时刻,你会发现一个性质,每一棵子树中车的数量一定不小于子树大小,而且多出来的部分也不会超过这棵子树到根的距离。题目所给的 “随机数据” 的性质就在这里体现了,每个点到根的距离期望值不会超过log。
我们设 \(f[u][i]\) 表示 u 这棵子树中车比点多了 i 个的方案数,儿子们多出来的点数加起来就是父亲多出来的点数,所以这是一个累加求和的背包问题。合并完之后我们要选择 u 这个父亲结点上车的数量,这时我们要让答案除以 \(b_u\),具体含义就是上一段所讲的内容。
#include <algorithm>
#include <iostream>
#include <cstring>
#include <cstdio>
#include <cmath>
#define FOR() ll le=e[u].size();for(ll i=0;i<le;i++)
#define QWQ cout<<"QwQ\n";
#define ll long long
#include <vector>
#include <queue>
#include <map>
using namespace std;
const ll N=501010;
const ll qwq=303030;
const ll inf=0x3f3f3f3f;
const ll p=998244353;
ll T;
ll n,m;
vector <ll> e[N];
ll dep[N];
ll f[N][123],g[123];
ll F[N],ni[N];
inline ll read() {
ll sum = 0, ff = 1; char c = getchar();
while(c<'0' || c>'9') { if(c=='-') ff = -1; c = getchar(); }
while(c>='0'&&c<='9') { sum = sum * 10 + c - '0'; c = getchar(); }
return sum * ff;
}
inline ll ksm(ll aa,ll bb) {
ll sum = 1;
while(bb) {
if(bb&1) sum = sum * aa %p;
bb >>= 1; aa = aa * aa %p;
}
return sum;
}
void qiu() {
F[0] = ni[0] = 1;
for(ll i=1;i<=N-10;i++) F[i] = F[i-1] * i %p;
ni[N-10] = ksm(F[N-10],p-2);
for(ll i=N-11;i>=1;i--) ni[i] = ni[i+1] * (i+1) %p;
}
void DFS(ll u) {
FOR() {
ll v = e[u][i];
dep[v] = dep[u] + 1;
DFS(v);
}
}
void TREE(ll u) {
for(ll v : e[u]) TREE(v);
f[u][0] = 1;
ll now = 0;
for(ll v : e[u]) {
memset(g,0,sizeof(g));
for(ll j=0;j<=dep[v];j++) {
for(ll k=0;k<=now;k++) {
if(j+k>dep[u]+1) break;
(g[j+k] += f[u][k] * f[v][j] %p) %= p;
}
}
now += dep[v];
for(ll j=0;j<=min(now,dep[u]+1);j++) f[u][j] = g[j];
}
memset(g,0,sizeof(g));
for(ll i=0;i<=dep[u]+1;i++) {
ll duo = i-1;
for(ll j=0;j<=now;j++) if(duo+j>=0 && duo+j<=dep[u]) (g[duo+j] += f[u][j] * ni[i] %p) %= p;
}
for(ll i=0;i<=dep[u];i++) f[u][i] = g[i];
}
int main() {
int x;
qiu();
n = read();
for(ll i=2;i<=n;i++) {
x = read();
e[x].push_back(i);
}
DFS(1);
TREE(1);
cout<<(f[1][0]*F[n])%p;
return 0;
}
E:
这题我们捏了个很有趣的东西哈哈,我们称之为 “左偏笛卡尔树”。
我们的思路是酱紫的:
首先看字典序最小的拓扑排序,我们找到最大的那个数字,连一条边让它指向右边的整体,表示原图中肯定是先访问了这个点才能访问右边的那些点,否则这个数字就不会出现在这里。
而它左边的哪些数字呢,我们把它们视作并列的兄弟关系,因为即使它们之间相互不连边,在最小拓扑排序中数大的点依旧是后访问。
因此我们可以递归地构建一棵树:找到区间中最大的数,左边的整体成为它的兄弟,右边的整体成为它的儿子。
左右区间各找最大点成为儿子,这样构造的树是标准的笛卡尔树,而我们这个是右边成为儿子,左边成为兄弟,我们形象地称之为 “左偏笛卡尔树”。
然后是最大的拓扑排序,原理是一样的,我们继续构造一棵树,把两棵树的所有边加在一起,就是答案的图。(这里要注意第二棵树的边往第一棵树里加时,不允许出现右边的点连向左边这种情况,可以用第一棵树的dfn序来判断,若有这种边直接不合法)
正确性怎么证明呢?
首先是必要性:想要得到题目所给的最小最大拓扑序,我们必须存在这些边。因为我们建图就是为了满足这样的性质,有这些边才能导致这样的顺序。
然后是充分性:只要我们有了这两棵树的所有边,我们就能得到题目所给的最小最大拓扑序。因为在任意一棵树种不存在右侧连向左侧的边,且兄弟结点从左到右依次增大,我们总是会先访问左侧的点,然后是左侧点的儿子(它们比父亲结点优先级更高,因而肯定也要比父亲的右兄弟优先级高),进而访问右侧点,因此一定会得到题目所给的顺序。
证毕。
代码实现:递归造树,RMQ或线段树查询区间最大最小值。
#include <algorithm>
#include <iostream>
#include <cstring>
#include <cstdio>
#include <cmath>
#define FOR() int le=e[u].size();for(int i=0;i<le;i++)
#define QWQ cout<<"QwQ\n";
#define ll long long
#include <vector>
#include <queue>
#include <map>
#define ls now<<1
#define rs now<<1|1
using namespace std;
const int N=801010;
const int qwq=303030;
const int inf=0x3f3f3f3f;
int T;
int n,m;
int a[N],b[N];
struct E{
int mx,mi,idx,idi;
}t[N<<2],ling;
vector <int> e[N];
int du[N];
int vis[N];
int st1[N],st2[N],cnt;
int dfn[N],tim;
inline int read() {
int sum = 0, ff = 1; char c = getchar();
while(c<'0' || c>'9') { if(c=='-') ff = -1; c = getchar(); }
while(c>='0'&&c<='9') { sum = sum * 10 + c - '0'; c = getchar(); }
return sum * ff;
}
void add(int u,int v) {
e[u].push_back(v);
du[v]++;
}
E pushup(E A,E B) {
E C = ling;
if(A.mx > B.mx) C.mx = A.mx, C.idx = A.idx;
else C.mx = B.mx, C.idx = B.idx;
if(A.mi < B.mi) C.mi = A.mi, C.idi = A.idi;
else C.mi = B.mi, C.idi = B.idi;
return C;
}
void built(int now,int l,int r) {
if(l==r) { t[now] = {a[l],a[l],l,l}; return ; }
int mid = l+r >> 1;
built(ls, l, mid);
built(rs, mid+1, r);
t[now] = pushup(t[ls],t[rs]);
}
E query(int now,int l,int r,int x,int y) {
if(x<=l && r<=y) return t[now];
E res = ling;
int mid = l+r >> 1;
if(x<=mid) res = pushup( query(ls, l, mid, x, y), res );
if(y>mid) res = pushup( query(rs, mid+1, r, x, y), res );
return res;
}
void solve(int fa,int l,int r,int cl) {
if(l>r) return ;
E wo = query(1, 1, n, l, r);
if(cl==1) {
add(fa,wo.mx);
solve(wo.mx, wo.idx+1, r, cl);
solve(fa, l, wo.idx-1, cl);
}
else {
if(dfn[wo.mi]<dfn[fa]) { cout<<"No\n"; exit(0); }
add(fa,wo.mi);
solve(wo.mi, wo.idi+1, r, cl);
solve(fa, l, wo.idi-1, cl);
}
}
void DFS(int u) {
vis[u] = 1;
FOR() {
int v = e[u][i];
if(vis[v]) continue;
du[v]--;
if(!du[v]) DFS(v);
}
}
void TREE(int u) {
dfn[u] = ++tim;
for(int i=e[u].size()-1;i>=0;i--) {
TREE(e[u][i]);
}
}
int main() {
ling = {-inf,inf,0,0};
n = read();
for(int i=1;i<=n;i++) {
a[i] = read();
}
built(1, 1, n);
solve(0, 1, n, 1);
TREE(0);
for(int i=1;i<=n;i++) {
a[i] = read();
}
built(1, 1, n);
solve(0, 1, n, 2);
for(int i=0;i<=n;i++) {
if(!du[i] && !vis[i]) DFS(i);
}
for(int i=1;i<=n;i++) if(!vis[i]) {cout<<"No\n"; return 0;}
for(int i=1;i<=n;i++) {
for(int v : e[i]) {
st1[++cnt] = i; st2[cnt] = v;
}
}
cout<<"Yes\n";
cout<<cnt<<"\n";
for(int i=1;i<=cnt;i++) cout<<st1[i]<<" "<<st2[i]<<endl;
return 0;
}