2023/7/8 模拟赛
2023/7/8 模拟赛 赛后总结
赛时 T4 蛮可惜的,推的其实差不多了,就是偏了一点点…… T2 忘了骗分了,有点可惜。
T1 礼物
题目描述
小T要给他的妹妹买礼物。他会不断的买,直到自己身上没有足够的钱来买任何一件礼物为止,他想知道有多少种方案符合他买礼物的方式。
我们认为两种选择方案不同当且仅当它们选取的的物品的集合不同。
因为答案可能很大,你只需要输出答案对\(10^7+7\)取模的结果即可。
思路
首先这道题有一点很重要:必须买到买不了其他物品为止。我们就从这里入手。我们考虑,如果买东西买到买不了其他物品,当且仅当当前未购买的物品中,价值最小的物品的花费要大于剩余钱数。我们直接去做背包显然不现实,因为我们无法确定是否到达最后的状态;这时,我们发现第二个性质,就是如果无法继续购买,那么价值小于(或者某些等于)当前未购买的价值最小的物品的所有物品都应该已经被买走。
这就启发我们,将物品按价值从小到大排序,然后让我们去从小到大枚举这个最后买不了的东西,这样,它之前的所有的物品都应该已经被买走,而它本身未被买走只能说明是后面的物品购买造成了影响。这时,我们对后面的物品做背包就好。我们令 \(f_i\) 表示买 \(i\) 元钱的东西的方案数,枚举后面的每个价值 \(c_j\),有 \(f_i = \sum f_{i-c_j}\)。最后,我们需要计入答案的就是剩余钱数小于当前枚举物品价值的 \(f_i\)。
但是,如果每次都做一遍背包,这样总复杂度就是 \(n^2m\) 的虽然随机数据有人水过去了。我们考虑优化。机房另一位大佬提供了一个思路,就是从后往前枚举,这样可以不用每次重新做背包,只需要去不断更新就好。我赛场上反正没想到,但是有了一种也不失优秀,并且比较巧妙的做法。
我们发现,我们每次都清空 \(f\) 比较浪费,那有没有办法只去除没有用的贡献呢?我们考虑 dp 过程:每次,我们都是通过前面的某个 \(f_i\) 值转移到 \(f_j\) 这里,那是不是,我们每次把 \(f_j\) 减走原来 \(f_i\) 造成的贡献就行了?又因为我们是从左到右枚举进行的 dp,那么我们再从左到右枚举,每次遇到一个非零的值,就把它造成的转移贡献减走就可以了。这样做后,复杂度变成了优秀的 \(nm\)。
代码:
点击查看代码
#include<bits/stdc++.h>
#define ll long long
using namespace std;
const int N = 1050, mod = 10000007;
inline int read(){
int x = 0, f = 1; char ch = getchar();
while(ch<'0' || ch>'9'){if(ch == '-') f = -1; ch = getchar();}
while(ch>='0'&&ch<='9'){x = x*10+ch-48, ch = getchar();}
return x * f;
}
int n, m;
int f[N];
int a[N];
ll sum;
int ans;
int main(){
n = read(), m = read();
for(int i = 1; i<=n; ++i){
a[i] = read();
sum+=a[i];
}
if(sum<=m){//特判一,没啥好说的(
puts("1");
return 0;
}
sort(a+1, a+n+1);
int st = 1;
while(a[st] == 0){//特判二,注意去掉开头的 0(否则会有 bug)
++st;
}
f[0] = 1;
for(int i = st; i<=n; ++i){
for(int j = m; j>=a[i]; --j){
f[j] = (1ll*f[j]+f[j-a[i]])%mod;
}
}
int lst = m;
for(int i = st; i<=n; ++i){
for(int j = 0; j<=m; ++j){//去除贡献
if(f[j] && j+a[i]<=m){
f[j+a[i]] = ((f[j+a[i]]-f[j])%mod+mod)%mod;
}
}
for(int j = m; j>max(m-a[i], -1); --j){//统计答案
ans = (1ll*ans+f[j])%mod;
}
m-=a[i];
if(m<0) break;
}
ans = (ans%mod+mod)%mod;
printf("%lld\n", ans);
return 0;
}
T2 课程
考场上没想出来……有人写状压+骗分拿了 70 pts,正解是随机化。
还没改出来,咕咕咕~
T3 火车
题面描述
A国有 \(n\) 个城市,城市之间有一些双向道路相连,并且城市两两之间有唯一路径。现在有火车在城市 \(a\) ,需要经过 \(m\) 个城市。火车按照以下规则行驶:每次行驶到还没有经过的城市中在 \(m\) 个城市中最靠前的。现在想知道火车经过这 \(m\) 个城市后所经过的道路数量。
参考洛谷 P3258 松鼠的新家,树链剖分+树上差分随便搞搞就行。实测 $n \log^2n $ 拿线段树整可过。
代码:
点击查看代码
#include<bits/stdc++.h>
#define ls tr<<1
#define rs tr<<1 | 1
#define ll long long
using namespace std;
const int N = 500050, M = 400040;
inline int read(){
int x = 0, f = 1; char ch = getchar();
while(ch<'0' || ch>'9'){if(ch == '-') f = -1; ch = getchar();}
while(ch>='0'&&ch<='9'){x = x*10+ch-48, ch = getchar();}
return x * f;
}
int head[N], tot;
struct node{
int nxt, to;
}edge[N<<1];
void add(int u, int v){
edge[++tot].nxt = head[u];
edge[tot].to = v;
head[u] = tot;
}
int dep[N], siz[N], dfn[N], top[N], totd, fa[N], son[N];
void dfs1(int u, int fath){
siz[u] = 1;
fa[u] = fath;
dep[u] = dep[fath]+1;
for(int i = head[u]; i; i = edge[i].nxt){
int v = edge[i].to;
if(v == fath) continue;
dfs1(v, u);
siz[u]+=siz[v];
if(siz[son[u]]<siz[v]) son[u] = v;
}
}
void dfs2(int u, int Top){
top[u] = Top;
dfn[u] = ++totd;
if(!son[u]) return;
dfs2(son[u], Top);
for(int i = head[u];i ; i = edge[i].nxt){
int v = edge[i].to;
if(!dfn[v]) dfs2(v, v);
}
}
bool tree[N<<2], lazy[N<<2];
void push_down(int tr){
if(lazy[tr]){
tree[ls] = 1;
tree[rs] = 1;
lazy[ls] = lazy[rs] = 1;
lazy[tr] = 0;
}
}
void modify(int tr, int L, int R, int lq, int rq, int val){
if(lq<=L && R<=rq){
tree[tr] = val;
lazy[tr] = val;
return;
}
push_down(tr);
int mid = (L+R)>>1;
if(lq<=mid) modify(ls, L, mid, lq, rq, val);
if(mid<rq) modify(rs, mid+1, R, lq, rq, val);
}
bool query(int tr, int L, int R, int pos){
if(L == R){
return tree[tr];
}
push_down(tr);
int mid = (L+R)>>1;
if(pos <= mid) return query(ls, L, mid, pos);
else return query(rs, mid+1, R, pos);
}
inline int LCA(int x, int y){
while(top[x] != top[y]){
if(dep[top[x]] < dep[top[y]]) swap(x, y);
modify(1, 1, totd, dfn[top[x]], dfn[x], 1);
x = fa[top[x]];
}
if(dep[x]>dep[y]) swap(x, y);
modify(1, 1, totd, dfn[x], dfn[y], 1);
return x;
}
int n, m, st;
//int city[M];
long long w[N];
ll ans = 0;
void dfs3(int u, int fath){
for(int i = head[u]; i; i = edge[i].nxt){
int v = edge[i].to;
if(v == fath) continue;
dfs3(v, u);
w[u]+=w[v];
}
ans+=w[u];
}
int main(){
n = read(), m = read(), st = read();
for(int i = 1;i<n; ++i){
int u = read(), v = read();
add(u, v);
add(v, u);
}
dfs1(1, 0);
dfs2(1, 1);
int u = st;
for(int i = 1; i<=m; ++i){
int v = read();
if(query(1, 1, totd, dfn[v])) continue;
int lca = LCA(u, v);
w[u]+=1;
w[v]+=1;
w[lca]-=2;
u = v;
}
dfs3(1, 0);
printf("%lld\n", ans);
return 0;
}
T4 计算
题面描述
给定 \(n\),求合法的 \((x_1,x_2,x_3,...,x_{2m})\) 组数。一组 \(x\) 是合法的,当且仅当
\(\forall i\in [ 1,2m ] ,x_i \in Z^+,x_i | n\)
\(\prod _{i=1}^{2m}x_i\le n^m\)
合法的 \((x_1,x_2,x_3,\dots,x_{2m})\) 可能很多,请你输出方案数 \(\mod\ 998244353\)。
思路
这个题可能是最有意思的一道题。首先我们来推式子。
我们设 \(F(x) = \prod _{i=1}^{2m}x_i\),\(G(x) = \prod _{i=1}^{2m} \frac{n}{x_i}\),显然 \(F(x)G(x) = n^{2m}\),而 \(F(x) \leq n^{2m}\),移项得 $G(x) \geq n^{2m} $。
我们令 \(S_1\) 表示 $F(x) < n^{2m} $ 的方案数,\(S_2\) 表示 $F(x) = n^{2m} $ 的方案数,\(S_3\) 表示 \(F(x)> n^{2m}\) 的方案数。
由我们上面推得的性质,对于每一个 \(S_1\) 中的方案,必然对应着一个 \(S_3\) 中的方案,也就是说,\(S_1 = S_3\),这样,我们只需要处理 \(S_2\) 就行了。
我们又发现,对于每个 \(n\) 的质因数都可以分开考虑,也就是,我们去考虑怎么把质因数去填到 这 \(2m\) 个 \(x_i\) 上。我们设 \(f_{i, j}\) 表示当前填到第 \(i\) 个 \(x\),填了 \(j\) 个质因数 \(p\),有转移 \(f_{i, j} = \sum_{k=0}^{k \leq m} f_{i-1, j-k}\)。
代码:
点击查看代码
#include<bits/stdc++.h>
using namespace std;
const int N = 6600, mod = 998244353;
int f[205][N];
int n, m;
int don = 1;
int s2 = 1;
void solve(int x, int num){
memset(f, 0, sizeof(f));
f[0][0] = 1;
for(int i = 1; i<=m*2; ++i){
for(int j = 0; j<=num*m; ++j){
for(int k = 0; k<=min(num, j); ++k){
f[i][j] = (f[i][j]+f[i-1][j-k])%mod;
}
}
}
don = (1ll*don*(num+1))%mod;
s2 = (1ll*s2*f[m*2][num*m])%mod;
}
inline int fpow(int a, int b){
a%=mod;
int ret = 1;
while(b){
if(b & 1){
ret = (1ll*ret*a)%mod;
}
b>>= 1;
a = (1ll*a*a)%mod;
}
return ret;
}
signed main(){
scanf("%d%d", &n, &m);
int cnt;
int nn = n;
for(int i = 2; i*i<=nn; ++i){
if(n%i == 0){
cnt = 0;
while(n%i == 0){
n/=i;
cnt++;
}
solve(i, cnt);
}
}
if(n>1){
solve(n, 1);
}
int ans = fpow(don, 2*m);
ans = (1ll*ans+s2)%mod;
ans = ((ans%2)?(ans+mod)/2:ans/2);//注意这里会除以二,需要特判。
printf("%d\n", ans%mod);
return 0;
}
总结:这次一道 ds,一道乱搞(bush),两道计数,考得还算满意吧,虽然也有些地方有遗憾就是了。