The 2024 ICPC Asia Nanjing Regional Contest
C - Topology
Solution
首先可以证明,对于节点数为 \(n\) 的树,整体拓扑序个数为 \(\dfrac{n!}{\prod_{i=1}^n size_i}\),其中树拓扑序是一种排列,使得每个节点的父亲都在它之前出现在排列中。简单证明一下:如果没有任何限制,那么方案数为 \(n!\),考虑子树 \(u\),那么 \(u\) 需要排在子树的最前面,所以乘以系数 \(\dfrac{1}{size_u}\).
下面的做法是基于从 \(v\) 开始从上到根的一条链进行考虑,我们钦定这条链上所有点在序列上的位置。为了复杂度,才从上至下 dp.
定义 \(f_u\) 为子树 \(u\) 的拓扑序个数,\(mul_u\) 为子树 \(u\) 所有节点的 \(size\) 之积。
定义 \(dp_{i,j}\) 为节点 \(i\) 在序列第 \(j\) 位,并且除了子树 \(i\) 其他部分都排序完成的方案数。这样的定义是由于我们需要至顶向下 dp.
考虑父节点 \(u\) 与子节点 \(v\),从 \(dp_{u,x}\) 转移到 \(dp_{v,y}\)。其实我们只需要考虑子树 \(u\) 除去子树 \(v\) 和节点 \(u\) 的部分:首先选出这一部分在序列中的位置,由于需要排在节点 \(u\) 之后,所以可选的位置只有 \(n-x-size_v\) 个,需要放置 \(size_u-size_v-1\) 个点,方案数为组合数
其次将这些节点内部拓扑序,方案数为
右边的乘式是因为左边式子分母中的 \(size_u\) 实际上应该为 \(size_u-size_v\).
将两个部分相乘之结果记为 \(\rm coe\),于是得到
这个复杂度为 \(\mathcal O(n^3)\),考虑优化。发现 \(\rm coe\) 实际上和 \(y\) 没有关系,于是 \(x,y\) 可以分开枚举,复杂度降到 \(\mathcal O(n^2)\).
Code
传送门。
I - Bingo
Solution
定义一行/列的 Bingo 数为该行/列的最大值。那么对于一种指派,最小的 Bingo 数就是所有行和列的 Bingo 数的最小值。先求最大值再求最小值是不容易的,我们考虑 min-max 容斥,计算最大值的最大值。
于是枚举子集 \(T\),假设选择了 \(x\) 行 \(y\) 列,则一共选择了 \(c=mx+ny-xy\) 个元素,选择行列方案 \(\dbinom{n}{x}\dbinom{m}{y}\).
将序列从小到大排列,假设 \(c\) 个元素的最大值为 \(a_i\),则前 \(i-1\) 个元素要选择 \(c-1\) 个,然后 \(c\) 个元素全排列,剩下 \(nm-c\) 个元素全排列,最后的方案数即为 \(\dbinom{i-1}{c-1}c!(nm-c)!\).
于是
此时复杂度为 \(\mathcal O((nm)^2)\).
考虑如何快速计算 \(\dbinom{i-1}{c-1}c!\cdot a_i\) 这一部分。
考虑快速计算 \(\dfrac{(i-1)!}{(i-c)!}\cdot a_i\).
设 \(f_i=(i-1)!\cdot a_i\),\(g_j=\dfrac{1}{(-j)!}\),记 \(i+j=c\),则对 \(f,g\) 做卷积,结果 \(h_c\) 就是答案。不过为了将 \(j\) 修正成正数,不妨记 \(g_j=\dfrac{1}{(nm-j)!}\),记 \(i+j+nm=c+nm\),则对 \(f,g\) 做卷积,结果 \(h_{c+nm}\) 就是答案。
复杂度 \(\mathcal O(nm\log(nm))\).
Code
# include <cstdio>
# include <cctype>
# define print(x,y) write(x), putchar(y)
template <class T>
inline T read(const T sample) {
T x=0; bool f=0; char s;
while(!isdigit(s=getchar())) f|=(s=='-');
for(; isdigit(s); s=getchar()) x=(x<<1)+(x<<3)+(s^48);
return f? -x: x;
}
template <class T>
inline void write(T x) {
static int writ[50], w_tp=0;
if(x<0) putchar('-'), x=-x;
do writ[++w_tp]=x-x/10*10, x/=10; while(x);
while(putchar(writ[w_tp--]^48), w_tp);
}
# include <set>
# include <cmath>
# include <vector>
# include <cstring>
# include <iostream>
# include <algorithm>
using namespace std;
typedef long long ll;
typedef pair <int,int> pii;
const int MAXN = 2e5+5;
const int MAXM = 1e6+5;
const int mod = 998244353, G = 3;
int n, m, a[MAXN], L, lim, iG, rev[MAXM];
ll fac[MAXN], ifac[MAXN], f[MAXM], g[MAXM];
ll qkpow(ll x, int y=mod-2) {
ll r = 1;
while(y) {
if(y&1) r=r*x%mod;
x=x*x%mod; y>>=1;
} return r;
}
void init() {
fac[0] = 1;
for(int i=1; i<=MAXN-5; ++i)
fac[i] = fac[i-1]*i%mod;
ifac[MAXN-5] = qkpow(fac[MAXN-5]);
for(int i=MAXN-6; i>=0; --i)
ifac[i] = ifac[i+1]*(i+1)%mod;
}
ll C(int x, int y) {
return fac[x]*ifac[y]%mod*ifac[x-y]%mod;
}
void preWork() {
int bit = 0; lim=1;
iG = qkpow(G);
while(lim<L+L-1) lim<<=1, ++bit;
for(int i=0; i<lim; ++i)
f[i] = g[i] = 0,
rev[i] = (rev[i>>1]>>1)|((i&1)<<bit-1);
}
void NTT(ll *f, bool opt) {
int wn, w, tmp;
for(int i=0; i<lim; ++i)
if(i<rev[i]) swap(f[i], f[rev[i]]);
for(int mid=1; mid<lim; mid<<=1) {
wn = qkpow(opt?G:iG, (mod-1)/(mid<<1));
for(int i=0; i<lim; i+=(mid<<1)) {
w = 1;
for(int j=0; j<mid; ++j, w=1ll*w*wn%mod) {
tmp = f[i+j+mid]*w%mod;
f[i+j+mid] = (f[i+j]-tmp+mod)%mod;
f[i+j] = (f[i+j]+tmp)%mod;
}
}
}
}
void noSananoLife() {
n=read(9), m=read(9), L=n*m+1;
for(int i=1; i<L; ++i)
a[i] = read(9);
sort(a+1, a+L);
preWork();
for(int i=1; i<L; ++i)
f[i] = fac[i-1]*a[i]%mod;
f[0] = 0;
for(int i=0; i<L; ++i)
g[i] = ifac[L-1-i];
NTT(f, 1), NTT(g, 1);
for(int i=0; i<lim; ++i)
f[i] = f[i]*g[i]%mod;
NTT(f, 0);
int _inv = qkpow(lim);
for(int i=0; i<lim; ++i)
f[i] = f[i]*_inv%mod;
ll ans = 0;
for(int x=0; x<=n; ++x)
for(int y=0; y<=m; ++y) {
if(x==0 && y==0) continue;
int c = m*x+n*y-x*y;
ll tmp = C(n,x)*C(m,y)%mod*fac[L-1-c]%mod*c%mod;
tmp = tmp*f[c+L-1]%mod;
if((x+y-1)&1) ans = (ans-tmp+mod)%mod;
else ans = (ans+tmp)%mod;
}
print(ans, '\n');
}
int main() {
init();
for(int T=read(9); T; --T)
noSananoLife();
return 0;
}
M - Ordainer of Inexorable Judgment
Solution
先抛开 \((x_0,y_0)\) 不管,我们计算那维莱特转一圈的时间内,有多少时间可以喷到多边形。从多边形整体的顺时针方向开始逆时针转,可以发现,我们只需要计算那维莱特的水柱第一次碰到多边形的角度 \(\alpha\),和那维莱特的水柱在这一圈最后碰到多边形的角度 \(\beta\).
设以 \((0,0)\) 为圆心,\(d\) 为半径的圆为圆 L。对于多边形的每个点 \((x_i,y_i)\),向圆 L 做两条切线,这两条切线分别代表了那维莱特第一次碰到和离开这个点的水柱(两条水柱的左边界和右边界)。由于一条水柱的中间射线和左边界右边界都平行,我们使用中间射线和 \(x\) 正半轴的夹角来表示一条水柱。于是对于 \(n\) 个点,我们可以计算出 \(2n\) 条水柱。
现在只需要找出 \(\alpha,\beta\)。注意到多边形和圆 L 没有交点,所以从 \(\beta\) 逆时针转到 \(\alpha\) 的角度一定 \(>180^\circ\)。所以将 \(2n\) 条水柱逆时针排序,一定只有一组解满足这个条件,我们就找到了答案。
Code
# include <cstdio>
# include <cctype>
# define print(x,y) write(x), putchar(y)
template <class T>
inline T read(const T sample) {
T x=0; bool f=0; char s;
while(!isdigit(s=getchar())) f|=(s=='-');
for(; isdigit(s); s=getchar()) x=(x<<1)+(x<<3)+(s^48);
return f? -x: x;
}
template <class T>
inline void write(T x) {
static int writ[50], w_tp=0;
if(x<0) putchar('-'), x=-x;
do writ[++w_tp]=x-x/10*10, x/=10; while(x);
while(putchar(writ[w_tp--]^48), w_tp);
}
# include <set>
# include <cmath>
# include <vector>
# include <cstring>
# include <iostream>
# include <algorithm>
using namespace std;
typedef long long ll;
typedef pair <int,int> pii;
const int MAXN = 2e5+5;
const int MAXM = 1e6+5;
const int mod = 998244353, G = 3;
const double pi = acos(-1.0);
const double eps = 1e-9;
int sgn(double x) {
return (fabs(x)<=eps)? 0: (x<0? -1: 1);
}
struct vec {
double x, y;
vec(const double X=0, const double Y=0): x(X), y(Y) {}
vec(double r, double alpha, int _): x(r*cos(alpha)), y(r*sin(alpha)) {}
vec operator + (const vec& t) const { return vec(x+t.x, y+t.y); }
vec operator - (const vec& t) const { return vec(x-t.x, y-t.y); }
friend double dot(const vec& a,const vec& b) { return a.x*b.x+a.y*b.y; }
double len() const { return sqrt(dot(*this, *this)); }
double angle() const {
double ang = atan2(y, x);
if(sgn(ang)<0) ang += pi*2;
return ang;
}
} st, now;
double d, t;
int n;
vector <double> seq;
int main() {
int x_0, y_0;
n=read(9), x_0=read(9), y_0=read(9);
d=read(9), t=read(9);
st = vec(x_0, y_0);
for(int i=1; i<=n; ++i) {
x_0=read(9), y_0=read(9);
now = vec(x_0, y_0);
double phi = acos(d/now.len());
seq.push_back((now-vec(d, now.angle()+phi, 1)).angle());
seq.push_back((now-vec(d, now.angle()-phi, 1)).angle());
}
double angs = st.angle();
for(int i=0; i<n*2; ++i) {
seq[i] -= angs;
if(seq[i] < 0) seq[i] += pi*2;
}
sort(seq.begin(), seq.end());
bool flag = true;
double L, R;
if(seq[0]+pi > seq.back()) {
L=seq[0], R=seq.back();
flag = false;
} else {
for(int i=0; i<n*2-1; ++i) {
if(seq[i]+pi < seq[i+1]) {
L=seq[i], R=seq[i+1];
break;
}
}
}
double q = floor(t/(pi*2));
double r = fmod(t, pi*2);
double ans = q*(R-L);
if(r > L) ans += r-L;
if(r > R) ans -= r-R;
ans = (flag? t-ans: ans);
printf("%.9f\n", ans);
return 0;
}
F - Subway
Solution
使用 \((i,j)\) 表示当前在第 \(i\) 个站,在第 \(j\) 条线路上。这个问题实际上就是计算最短路。问题在于,这张图的边数是 \(n^2\) 条级别的,且边数的复杂度主要在于同站换线路操作。于是我们的核心思路就是降低同站换线路的边数。
假设我们从 \((i,x)\) 开始进行同站转移,可以转移到 \(y,z\) 两条线路。不妨设 \(a_y<a_z\)。于是线路 \(y\) 的转移 \(d_{(x,y)}=dis[(i,x)]+b_x\cdot a_y\),线路 \(z\) 的转移 \(d_{(x,z)}=dis[(i,x)]+b_x\cdot a_z\)。
我们可以证明,仅考虑同站转移时,从 \(z\) 转移到 \(y\) 一定不优。这是因为 \(d_{(z,y)}=dis[(i,z)]+b_z\cdot a_y=d_{(x,z)}+b_z\cdot a_y\)。由于 \(a_y<a_z\),所以 \(d_{(x,y)}<d_{(x,z)}<d_{(z,y)}\)。这就意味着,从 \((i,x)\) 转移,我们只需要转移到同站未出队列的 \(a_y\) 最小的 \(y\) 进行转移。
同站转移使用李超树进行维护即可。
Code
# include <cstdio>
# include <cctype>
# define print(x,y) write(x), putchar(y)
template <class T>
inline T read(const T sample) {
T x=0; bool f=0; char s;
while(!isdigit(s=getchar())) f|=(s=='-');
for(; isdigit(s); s=getchar()) x=(x<<1)+(x<<3)+(s^48);
return f? -x: x;
}
template <class T>
inline void write(T x) {
static int writ[50], w_tp=0;
if(x<0) putchar('-'), x=-x;
do writ[++w_tp]=x-x/10*10, x/=10; while(x);
while(putchar(writ[w_tp--]^48), w_tp);
}
# include <set>
# include <map>
# include <tuple>
# include <queue>
# include <vector>
# include <cstring>
# include <iostream>
# include <algorithm>
using namespace std;
typedef long long ll;
typedef pair <int,int> pii;
const int MAXN = 2e5+5;
const ll infty = 1e18;
map <pii, bool> vis;
map <pii, ll> dis;
map <pii, pii> nxt;
vector <int> value;
ll ans[MAXN];
int n, k, a[MAXN], b[MAXN], rt[MAXN];
struct Node {
int path, val;
Node() {}
Node(int _p=0, int _v=0): path(_p), val(_v) {}
bool operator < (const Node& t) const {
return (val^t.val)? (val<t.val): (path<t.path);
}
};
struct nodeq {
int pos, path; ll dis;
nodeq() {}
nodeq(int _pos=0, int _p=0, ll _d=0): pos(_pos), path(_p), dis(_d) {}
bool operator < (const nodeq& t) const {
return dis > t.dis;
}
};
set <Node> cho[MAXN];
priority_queue <nodeq> q;
struct lichaoTree {
struct line {
ll k, b;
line(ll _k=0, ll _b=0) {
k=_k, b=_b;
}
ll calc(int x) { return k*value[x]+b; }
};
static const int MAXM = MAXN*30;
int idx, son[MAXM][2];
line t[MAXM];
void ins(int &o, int l, int r, line now) {
if(!o) return t[o=++idx] = now, void();
int mid = l+r>>1;
ll La = t[o].calc(l), Ra = t[o].calc(r), Lb = now.calc(l), Rb = now.calc(r);
if(La<=Lb && Ra<=Rb) return;
if(La>=Lb && Ra>=Rb) return t[o]=now, void();
if(t[o].calc(mid) > now.calc(mid)) swap(t[o], now);
if(t[o].calc(l) < now.calc(l))
ins(son[o][1], mid+1, r, now);
else ins(son[o][0], l, mid, now);
}
ll ask(int o, int l, int r, int p) {
if(!o) return infty;
int mid = l+r>>1;
ll ret = t[o].calc(p);
if(p<=mid) ret=min(ret, ask(son[o][0], l, mid, p));
else ret=min(ret, ask(son[o][1], mid+1, r, p));
return ret;
}
} Seg;
int main() {
n = read(9), k = read(9);
for(int i=1; i<=k; ++i)
value.push_back(a[i]=read(9));
for(int i=1; i<=k; ++i) b[i]=read(9);
vector <int> scan;
for(int i=1; i<=k; ++i) {
int all = read(9); scan.clear();
for(int j=1; j<=all*2-1; ++j)
scan.push_back(read(9));
for(int j=1; j<all; ++j)
nxt[make_pair(scan[j*2-2], i)] = make_pair(scan[j<<1], scan[j*2-1]);
for(int j=1; j<=all; ++j)
cho[scan[j*2-2]].insert(Node(i, a[i]));
}
for(auto it: cho[1]) {
pii now = make_pair(1, it.path);
dis[now] = 0;
q.push(nodeq(1, it.path, 0));
}
sort(value.begin(), value.end());
value.erase(unique(value.begin(), value.end()), value.end());
int lim = value.size()-1;
cho[1].clear();
while(! q.empty()) {
nodeq now = q.top(); q.pop();
pii u = make_pair(now.pos, now.path);
if(vis.find(u)!=vis.end()) continue;
vis[u] = true;
if(now.pos != 1) {
int val = lower_bound(value.begin(), value.end(), a[now.path])-value.begin();
ll ret = Seg.ask(rt[now.pos], 0, lim, val);
if(dis.find(u) == dis.end()) dis[u]=ret;
else dis[u] = min(dis[u], ret);
Seg.ins(rt[now.pos], 0, lim, {b[now.path], dis[u]});
cho[now.pos].erase(Node(now.path, a[now.path]));
if(! cho[now.pos].empty()) {
Node tmp = *cho[now.pos].begin();
val = lower_bound(value.begin(), value.end(), a[tmp.path])-value.begin();
ll ret = Seg.ask(rt[now.pos], 0, lim, val);
pii v = make_pair(now.pos, tmp.path);
if(dis.find(v)==dis.end() || dis[v]>ret) {
dis[v] = ret;
q.push(nodeq(v.first, v.second, dis[v]));
}
}
}
if(nxt.find(u) == nxt.end()) continue;
pii tmp = nxt[u];
pii v = make_pair(tmp.first, now.path);
if(dis.find(v)==dis.end() || (dis[v]>dis[u]+tmp.second)) {
dis[v] = dis[u]+tmp.second;
q.push(nodeq(v.first, v.second, dis[v]));
}
}
for(int i=1; i<=n; ++i) ans[i]=infty;
for(auto it: dis)
ans[it.first.first] = min(ans[it.first.first], it.second);
for(int i=2; i<=n; ++i)
print(ans[i], ' '); puts("");
return 0;
}

浙公网安备 33010602011771号