[考试记录] 2024.8.14 csp-s模拟赛20
[考试记录] 2024.8.13 csp-s 模拟赛20
90+39+0+0 还是太🥬。
T1 那一天我们许下约定
题面
那一天我们在教室里许下约定。 我至今还记得我们许下约定时的欢声笑语。我记得她说过她喜欢吃饼干,很在意自己体重的同时又控制不住自己。她跟我做好了约定:我拿走她所有的饼干共 \(N\) 块,在从今天起不超过 \(D\) 天的时间里把所有的饼干分次给她,每天给她的饼干数要少于 \(M\) 以防止她吃太多。 当然,我们的约定并不是饼干的约定,而是一些不可言状之物。 现今回想这些,我突然想知道,有多少种方案来把饼干分给我的她。
\(N\le 2000\),\(D\le 10^{12}\),\(M\le 2000\)
解析
挂掉10pts,南泵😑
看到 \(N\) 和 \(M\) 的数据范围,推测是个 \(N^2\) DP。再观察一下题目,一眼背包。
可以发现,每天的最小 有效 贡献为 \(1\),那么能产生有效贡献的天数就为 \(\min (N,D)\)。这样一来 \(D\) 的范围也被限制在了 \(2000\) 以内。为什么只说有效贡献呢?因为这道题是允许每天给 \(0\) 块饼干的,如果在背包里加入 \(0\) 这个物品是不会对最终方案数产生任何影响的。于是我们很容易地可以写出这么一个多重背包,每天加入的物品为 \(1\sim m-1\),复杂度 \(\mathcal{O}(N^3)\)。
for(int i=1; i<=dm; ++i){
for(int j=1; j<m; ++j){
for(int z=n; z>=j; --z){
g[z] = (f[z-j] + g[z]) % M;
}
}
}
现在考虑如何计算答案。考虑到我的第一重循环是在循环模拟天数,那么每一天的背包下来都能对答案产生一定的贡献(g[n]
)。在 \(D\) 天当中,有 \(i\) 天是能产生有效贡献的,那么剩下的天数就填 \(0\) 就好了。所以,每一天的方案数贡献为 \(g[n]\times \binom{D}{i}\)。
发现 \(D\le10^{12}\) 数据过大不好计算,注意到当 \(i=1\) 的时候 \(\binom{D}{i}=D\),\(i=2\) 时 \(\binom{D}{i}=\frac{D(D-1)}{i}\)。那么可以每次累加 \((D-i+1)\times {\text inv}_i\) 即可。
但是 \(\mathcal{O}(N^3)\) 的复杂度还是太高了,考虑优化。仔细观察最里边的那层循环,发现对于 f
数组的 f[i]
可以对他之后连续的 \(m-1\) 长度的段产生贡献,考虑差分,即在 \(i+1\) 的位置加上 \(f[i]\),在 \(i+m\) 的位置减去 \(f[i]\),即可优化一维。复杂度 \(\mathcal{O}(N^2)\)。
code
#include<bits/stdc++.h>
using namespace std;
#define ll long long
constexpr int N = 2005, M = 998244353;
int n, m, f[N], g[N], dm, ans, inv[N+5], p[N+5];
ll d, mul;
inline int qpow(int a, int k){
int as = 1;
while(k){
if(k & 1) as = (ll)as * a % M;
a = (ll)a * a % M; k >>= 1;
} return as;
}
int main(){
ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
p[0] = 1; for(int i=1; i<=N; ++i) p[i] = (ll)p[i-1] * i % M;
inv[N] = qpow(p[N], M-2); for(int i=N-1; i>=1; --i) inv[i] = (ll)inv[i+1] * (i+1) % M;
while(cin>>n>>d>>m && !(n == 0 && d == 0 && m == 0)){
if(d * (m - 1) < n){
cout<<"0\n";
continue;
}
dm = min(d, (ll)n); m = min(m, n);
ll mul = d; f[0] = 1;
for(int i=1; i<=dm; ++i){
for(int j=0; j<=n; ++j){
g[j+1] = ((ll)g[j+1] + (ll)f[j]) % M;
int mn = min(n+1, j+m);
g[mn] = (((ll)g[mn] - (ll)f[j]) % M + M) % M;
}
for(int i=1; i<=n; ++i) g[i] = ((ll)g[i] + (ll)g[i-1]) % M;
ans = ((ll)ans + (ll)g[n] * mul % M * inv[i]) % M;
mul = (__int128)mul * max(d - i, 1ll) % M;
for(int i=0; i<=n; ++i) f[i] = g[i], g[i] = 0;
} cout<<ans<<'\n';
memset(f, 0, sizeof f);
memset(g, 0, sizeof g);
ans = 0;
} return 0;
}
注意细节,\(n\) 可能为 \(0\),所以退出条件为 !(n == 0 && d == 0 && m == 0)
。因此挂掉 10pts。
T2 那一天她离我而去
题面
她走的悄无声息,消失的无影无踪。 至今我还记得那一段时间,我们一起旅游,一起游遍山水。到了最终的景点,她却悄无声息地消失了,只剩我孤身而返。 现在我还记得,那个旅游区可以表示为一张由 \(n\) 个节点 \(m\) 条边组成无向图。我故地重游,却发现自己只想尽快地结束这次旅游。我从景区的出发点(即 \(1\) 号节点)出发,却只想找出最短的一条回路重新回到出发点,并且中途不重复经过任意一条边。 即:我想找出从出发点到出发点的小环。
\(n\le 10^4\),\(m\le 4*10^4\)
解析
暴力碾压标算的乱搞题
这道题最优解是暴力dfs + 优化。菊花和太阳图均0.1s出解。👌
先看基础暴力dfs,以 1 为起点暴搜,同时拿 flag 记录每一条走过的边。如果搜到 1,那就更新答案。这是最基础的。考虑优化。
- 优化一:设当前搜到了 \(u\) 这个点,并且走过的路径长度为 \(x\),\(u\sim 1\) 的最短路径长度为 \(dis\)。如果 \(x+dis>ans\),那么就不用搜了,根本不会对答案产生任何贡献。采用这个优化可以达到 92pts。
- 优化二:dfs的复杂度瓶颈在于搜的点的数量。考虑在这里弄点操作。我们发现,对于有一些点,他是不能和 1 成环的,那搜他也没啥用。所以考虑找到所有 可以和 1 成环的点。跑 tarjan 求边双,和 1 在同一个边双里的就是有效的点。100pts。
code
#include<bits/stdc++.h>
using namespace std;
constexpr int N = 1e4 + 5, M = 4e4 + 5;
#define p pair<int, int>
int T, n, m, h[N], cnt, dis[N], ans = INT_MAX, dfn[N], low[N], tot, st[N], sc, num[N], sh[N], scnt;
vector<int> sG[N];
bitset<N> vis;
bitset<M> flag;
template <typename T> inline void rd(T &x){
x = 0; char ch = getchar_unlocked();
while(!isdigit(ch)) ch = getchar_unlocked();
while(isdigit(ch)){
x = (x<<1) + (x<<3) + (ch^48);
ch = getchar_unlocked();
}
}
template <typename T2> inline void wt(T2 x){
if(x<0) putchar('-'), x = -x;
if(x/10 > 0) wt(x / 10);
putchar_unlocked(x%10 + '0');
}
struct edge{ int v, nxt, val, id; }e[M<<1], se[M<<1];
inline void add(int u, int v, int val, int id){
e[++cnt] = edge{v, h[u], val, id}; h[u] = cnt;
}
inline void sadd(int u, int v, int val, int id){
se[++scnt] = edge{v, sh[u], val, id}; sh[u] = scnt;
}
inline void tarjan(int u, int f){
dfn[u] = low[u] = ++tot;
st[++st[0]] = u;
for(int i=h[u]; i; i=e[i].nxt){
int v = e[i].v;
if(!dfn[v]){
tarjan(v, u);
low[u] = min(low[u], low[v]);
} else low[u] = min(low[u], dfn[v]);
}
if(dfn[u] == low[u]){
++sc; int v;
do{
v = st[st[0]--];
sG[sc].push_back(v);
num[v] = sc;
} while(v != u);
}
}
inline void dijkstra(){
priority_queue<p, vector<p>, greater<p> > q;
memset(dis, 0x7f, sizeof dis);
dis[1] = 0; q.push((p){0, 1});
while(!q.empty()){
p t = q.top(); q.pop();
int u = t.second;
if(vis[u]) continue;
vis[u] = 1;
for(int i=sh[u]; i; i=se[i].nxt){
int v = se[i].v;
if(dis[v] > dis[u] + se[i].val){
dis[v] = dis[u] + se[i].val;
q.push((p){dis[v], v});
}
}
}
}
inline void DP(int u, int x){
if(x + dis[u] > ans) return;
for(int i=sh[u]; i; i=se[i].nxt){
int v = se[i].v;
if(flag[se[i].id]) continue;
int y = x + se[i].val;
if(v == 1){
ans = min(ans, x + se[i].val);
continue;
}
flag[se[i].id] = 1;
DP(v, y);
flag[se[i].id] = 0;
}
}
int main(){
rd(T); while(T--){
rd(n), rd(m);
for(int i=1, x, y, z; i<=m; ++i){
rd(x), rd(y), rd(z);
add(x, y, z, i), add(y, x, z, i);
}
for(int i=1; i<=n; ++i) if(!dfn[i]) tarjan(i, 0);
for(int u : sG[num[1]]){
for(int i=h[u]; i; i=e[i].nxt){
int v = e[i].v;
if(num[v] != num[1]) continue;
else sadd(u, v, e[i].val, e[i].id);
}
}
dijkstra();
DP(1, 0);
if(ans == INT_MAX) wt(-1), putchar('\n');
else wt(ans), putchar('\n');
memset(h, 0, sizeof(h));
memset(sh, 0, sizeof(sh));
memset(dfn, 0, sizeof(int) * (n+1));
memset(low, 0, sizeof(int) * (n+1));
cnt = scnt = 0; vis.reset(); flag.reset();
ans = INT_MAX;
} return 0;
}
T3 哪一天她能重回我身边
题面
她依然在我不知道的地方做我不知道的事。
桌面上摊开着一些卡牌,这是她平时很爱玩的一个游戏。如今卡牌还在,她却不在我身边。不知不觉,我翻开了卡牌,回忆起了当时一起玩卡牌的那段时间。
每张卡牌的正面与反面都各有一个数字,我每次把卡牌按照我想的放到桌子上,而她则是将其中的一些卡牌翻转,最后使得桌面上所有朝上的数字都各不相同。
我望着自己不知不觉翻开的卡牌,突然想起了之前她曾不止一次的让我帮她计算最少达成目标所需要的最少的翻转次数,以及最少翻转达成目标的方案数。
(两种方式被认为是相同的当且仅当两种方式需要翻转的卡牌的集合相同)
如果我把求解过程写成程序发给她,她以后玩这个游戏的时候会不会更顺心一些?
多测。\(T\le 50\),\(n\le 10^5\)。
解析
场上没多想打了个状压竟然全 T 了。
现在看来,这种题不应该一眼建图吗?😉
考虑对每种纸牌正反面的数字连边,注意是有向的。当前纸牌情况合法当且仅当每种数字的出度为 \(1\)。为什么呢?因为位于正面的数字都会发出一条指向其它数字的边,所以每个数字的出度也就是每个数字在正面的数量。
那么可变为合法的情况当且仅当所有的联通块是树或者基环树。dfs一遍统计这个联通块里的点数和边数判断即可。
如何统计答案呢?首先正面向反面建有向边,并将边权记录为 1,反面向正面建有向边,边权记录为 0。
(没写完)