11.24 模拟赛题解
题解
题解就随便一点了,能看懂就行了
题目皆为原题,非原创,下文会标注原题出处
这次如果大家都不挂分的话应该280或者300以上都没问题,部分分给的很足,出得十分良心。
算是一场信心赛吧,码量都不算大,思维也不算难。
T1 打工job
这是一道十分常规的贪心,搞得我部分分都不知道怎么出了
\(n^2\)暴力给了\(50\) ,但我觉得没有人做不出来T1。
其实这题就是考验选手有没有轻敌,许多选手刚看到题可能会以为是一个简单模拟或是简单贪心,没有仔细考虑清楚就开始写代码,后果可能会拿不到满分,甚至拿不到高分。
这题虽说是一道常规贪心,却是贪心中一个经典套路,反悔贪心
看到题的第一步肯定是想排序截止时间
正常人当然是想优先完成紧急的工作啦~
在紧急的工作里自然是优先完成报酬高的工作啦~
如果你想到这样贪心的话
枚举1~n项已经排好序的工作
每做一项工作就day++
然后符合条件就做
但是这样就只能拿到一些分
其实错误的原因很简单(如样例2
到后期万一都是高报酬的工作(但由于超时做不了了)
你会不会后悔前期做了哪些低报酬的工作呢?
所以贪心应该有一个后悔操作,把之前赚钱最少的那个工作去掉,换成现在这个更高报酬的工作。实现可以使用优先队列
正确思路就是先按截止日期排序,然后如果一个工作有时间去做,就先做了它(听起来有点怪),然后把它的价值压入一个小根堆。当我们找到一个没法做却价值比当前堆顶高的工作时,我们就放弃那个最小的工作,用做它的时间去做这个价值更高的工作。
当然这道题我也发现了许多奇奇怪怪的做法,例如树状数组什么的,有兴趣可以研究一下。
原题:luogu P2949 [USACO09OPEN]Work Scheduling G
T2 购物shop
题意 :给你 \(n\) 种硬币类型,每种硬币类型 \(v_i\) 买家有 \(c_i\) 个,卖家有无限个,买家要买一个 \(m\) 元的东西(卖家可以找零),买家问你双方之间交流的硬币个数(指买家用的硬币数 + 卖家找零的硬币数)最少为多少个?
题意中的金额我们不妨抽象为线段长度
所以题意可以简化成:怎样拼凑成两段线段,且它们的长度之差等于T
你要怎么去拼凑线段,这就有些背包的感觉。所以我们可以分别从hyfhaha和店主不同的角度来考虑最优解
因为hyfhaha的硬币数是有限的,所以可以视为多重背包(这里考到了多重背包的二进制优化),求达到一定钱数所需的最少的硬币数量;店长的硬币数是无限的,所以可以视为完全背包,也是求同种方案最少的硬币数量,再在最后枚举每种付钱和找钱的方案硬币数之和,求个min即可得出答案
但是问题是,我们应该一直找到多少钱的方案才能保证找到合法的最优解呢?我认为这是这道题最关键的部分,以下为证明:
$ \text { 关于这里的 } m x, \text { 我们令它为 } \max {i=1}^{n} v^{2} $
假设, 在 \(m+m x\) 的范围内, 没有(买家付的钱\(-\)卖家找的钱等于 \(m\) )的情况, 那么卖家所找的硬币的个数必定大于 \(v_{\max } \quad\left(\right.\) 因为 \(v_{\max }^{2}\) 显然小于 \(\left.m x\right)\) 。设卖家找的钱的序列为 \(p\left(\forall p_{i}<p_{i+1}\right)\) 我们作一个前缀数组 \(s,\) 使 \(s_{i}=\sum_{j=1}^{j \leq i} p_{j},\) 根据同余的性质,必有两个(或两个以上)的 $s_i $是在 \(mod\ v_{max}\)意义下是同余的,(因为 \(s\) 序列长度大于 \(v_{max}\),而取模后的数最多有 \(v_{\max }\) 种 \() ,\) 那么必然有 \(s_{i}-s_{j}=k * v_{\max }(i>j)\) 即这部分的数可以替换成 \(k\) 个 \(v_{\max }\) .
那么, 既然卖家不用还超过 \(v_{\max }^{2}\) 的钱, 那么买家就不用付超过 \(m+v_{\max }^{2}\) 的钱, 证必。
那么这题也就没了,主要是两个背包,也算是一道比较不常规的背包题吧,但总体不难。
原题:P2851 [USACO06DEC]The Fewest Coins G
T3 魔法mogic
状压+线段树
这是一道神仙题,想拿满分并不简单,但部分分给的很足,可以考验选手考场时间分配等临场能力
代码实现起来也比较清真,放T3感觉还可以,思维难度不算大,有点考细节
简化题面
给定 \(m\) 个长度为 \(n\) 的可能含有 ?
的 01 串,其中 ?
既能代表 0
也能代表 1
, \(q\) 次操作,每次给定一个区间,求有多少 01 串满足区间内的所有字符串都可以解释成该 01 串,或者单点修改某个字符串。
Solution
子任务 1 :
乱搞
子任务 2 :
考虑 \(n\) 只有 \(10,\) 因此可以 \(O\left(2^{n}\right)\) 去枚举所有可能的串, 然后对于每个询问 \(O(m)\) 的逐个判定是否合 法。时间复杂度 \(O\left(q m 2^{n}\right)\)。
子任务 3 :
考虑对于一段所有字符串的第 \(x\) 个字符, 一共有四种可能:确定为 \(0 ;\) 确定为 \(1 ;\) 都可以; 都不可以。如
果 \(0 / 1\) 都不可以, 则答案为 \(0,\) 因为不会有任何一个字符串匹配该区间。如果确定为某个数, 则这一位只
有一种可能; 否则这一位有两种可能。根据乘法原理, 如果有 \(a\) 个位置有两种可能, 则本次询问的答案为 \(2^{a}\)。
因此对于每个询问, \(O(n m)\) 地去遍历区间内所有字符即可。时间复杂度 \(O(n m q)\)。
子任务 4 :
考虑 \(n\) 只有 \(30,\) 可以状压到 int 中。具体的, 维护两个 int,第一个 int 维护对应位是否确定是 0 或 \(1,\) 第二个 int 维护如果确定是 0 或 1 了那么具体是 0 还是 1 。
例如, 对于单个字符串, 它所有的为 \(?\) 的位置, 在第一个 int 中对应位置是 \(0,\) 所有为 0 或 1 的 位置, 在第个 int 中对应的位置是 \(1,\) 在第二个 int 中对应的位置是自身的值。
考虑 \(a_{1}, a_{2}\) 是询问的左端点到某个字符串之前所维护的两个 int \(, b_{1}, b_{2}\) 是该字符串的两个 int,现在合并这两个信息。
如果某一位置即不可以是 \(1,\) 也不可以是 \(0,\) 那么该字符串不为 \(?\) 的位置在 \(b_{2}\) 中对应的值应该至少有一 个和 \(a_{2}\) 中对应位置的值且 \(a_{1}\) 的该位置为 \(1,\) 位运算可以表现为 \(\left(a 1 \& b_{1}\right) \&\left(a_{2} \oplus b_{2}\right) \neq 0,\) 则该 询问的答案为 0 。
否则这两段信息可以合并:将他们已经确定字符的位置合并起来, 然后将确定位置对应的值合并起来即可。于是 \(a_{1}\) 对 \(b_{1}\) 取或, \(a_{2}\) 对 \(b_{2}\) 取或即可。
最终该旬问 \(0 / 1\) 都可以的位置的个数即为 \(a_{1}\) 中 1 的个数。
时间复杂度 \(O(m q)\) 。
子任务 5 :
由于 \(n\) 只有 \(1,\) 问题变成了求某个区间内的字符是不是全是 \(0,\) 全是 \(1,\) 全是 \(?\) 或 0 和 1 都有。 可以考虑用线段树非常轻松的维护这样的信息。
时间复杂度 \(O(q \log m)\) 。
子任务 6:
世界上没有什么事情是开一棵线段树不能解决的, 如果有, 那就开 \(30\) 棵。
时间复杂度 \(O(n q \log m)\)。
子任务 7 :
考虑结合子任务 4 和子任务 5 的做法, 发现两个区间的状压信息也可以用子任务 4 的方法合并。因此用线段树维护这两个 int 的状压信息即可。
时间复杂度 \(O(q \log m)\)。
T4 和平peace
一道树上计数题,应该都满熟悉这类题的了
部 分 分 给 得 特 别 多 !
就算你完全不会正解你也可以拿 \(65\) 分!
测试点 1-2
暴力枚举两条路径的起始点和终点,然后再判断是否相交。时间复杂度 \(O(n^4)\)
测试点 3-4
暴力枚举三条路径的起始点和终点,然后再判断是否相交。时间复杂度 \(O(n^6)\)
这两档部分分虽然很水,但在考场上写出来不算简单,所以给了较多的分数
方法1:暴力计算贡献
首先很显然的,用所有的方案减去不合法的方案
所有的方案就是:树上所有长度不超过\(k\)的路径条数的\(m\)次方
计算不合法的方案要围绕的基准点是什么呢?
不合法的方案路径一定有交点,考虑枚举交点,但路径交点可能有多个,所以可以枚举深度最浅的交点
首先求出\(f[p]\)表示经过\(p\)且路径两端都在\(p\)子树内的长度不超过\(k\)的方案数,\(g[p]\)表示经过\(p\)路径且两端分别在\(p\)子树内外,长度不超过\(k\)的方案数
那么以\(p\)为深度最浅的交点的不合法方案数就为:\(\displaystyle \sum_{i=0}^{m-1}\binom{m}{i}g[p]^i\times f[p]^{m-i}\)
也就是选出\(i\)个国家放到子树外面,注意不能放\(m\)只在外面,不然最浅的交点就不是\(p\)了。
这里时间复杂度为 \(O(nm)\).
考虑如何计算\(f\)和\(g\)
- 对于\(k\)一般的情况:\(O(n^2)\)
对于\(p\)的一个儿子\(q\),直接递归\(q\)的子树,之前的数据都存在前缀桶里,更新答案后,把新的数据扔到桶里就好了
计算\(g[p]\),其实就是把子树\(p\)以外的看成\(p\)的另外一棵子树,然后类似上面的过程计算就可以了
- 对于\(k=0\)的情况:\(O(n)\)
国家都只能在\(p\)这个节点上,\(f[p]=1,g[p]=0\)
- 对于\(k=n-1\)的情况:\(O(n)\)
此时国家可以走完一整棵树,设\(\displaystyle cal(x)=\binom{x}{2}+x\),表示在\(x\)个点中选出\(2\)个点满足终点不小于起点
\(\displaystyle f[p]=cal(size[p])-\sum_{q\in son(p)}cal(size[q])\quad g[p]=(n-size[p])\times size[p]\)
- 对于一条链的情况:\(O(n)\)
\(f[p]\)就是直接向后扩展,注意和\(k+1\)取\(\min\),\(+1\)是因为还可以取到自己
\(g[p]\)取的是前面所有能扩展的向后扩展,对\(f[p]\)求个前缀和,然后再减掉没有到达\(p\)的就可以了
这样最后总的时间复杂度是\(O(n^2+nm)\),代码中直接用快速幂,复杂度多个\(\log\)(反正也不是正解
方法2:简单的转换
之前的标算被踩了,本来没有这里的
方法\(1\)之所以慢,关键就在于那个求和式,需要\(O(m)\)的时间枚举
然后这个式子不就是二项式定理的形式吗???
所以\(\displaystyle ans=\sum_{i=0}^{m}\binom{m}{i}g[p]^if[p]^{m-i}-g[p]^m=(f[p]+g[p])^m-g[p]^m\)
这样就可以\(O(n\log m)\)计算了,然后就没了
但不妨从容斥的角度直接得到最后的答案
显然这\(m\)条路径全部相交的点,肯定是形成一条链(边形成的),这条链的长度为\(x\)
设\(f(x)\)表示这条链的长度为\(x\)的方案数,\(g(x)\)表示钦定这条链的长度为\(x\)的方案数,那么就有:
\(\displaystyle g(x)=\sum_{d\geq x}(d-x+1)f(d)\),直接展开得到:
\(g(0)=f(0)+2\times f(1)+3\times f(2)+...\)
\(g(1)=1\times f(1)+2\times f(2)+...\)
显然不合法的方案数就是\(\displaystyle f(0)+f(1)+...=g(0)-g(1)\)
那么只需要求出\(g(0)\)和\(g(1)\)就行了,因为\(g(1)\)肯定是两个相连的节点,直接设为每个节点和其父亲就行了
\(g(0)\)就表示,在树上选出\(m\)条长度小于等于\(k\)的链,且至少都经过同一个点
\(g(1)\)表示,在树上选出\(m\)条长度小于等于\(k\)的链,且至少都经过同两个点
最后要求的东西,其实就是方法\(1\)求出来的东西了
不知道有没有人能找到更优的求解 \(f\)和\(g\) 数组的算法
#include <iostream>
#include <cstdio>
#include <cstring>
#include <string>
#define ll long long
using namespace std;
const int N=1e6+10;
const ll mod=998244353;
int n,m,k,flag=1,tot,tail;
int fir[N],dep[N],que[N],size[N],s1[N],s2[N],cnt[N];
ll f[N],g[N],pre[N];
struct edge
{
int to;
int nxt;
}e[2*N];
inline void add(int x,int y)
{
e[++tot].to=y; e[tot].nxt=fir[x]; fir[x]=tot;
e[++tot].to=x; e[tot].nxt=fir[y]; fir[y]=tot;
}
inline ll power(ll a,int b)
{
ll res=1;
for(;b;b>>=1,a=a*a%mod)
if(b&1) res=res*a%mod;
return res;
}
inline ll cal(int x)
{
return 1ll*x*(x-1)/2+x;
}
inline void dfs1(int p,int fa)
{
if(p==0) return;
dep[p]=dep[fa]+1,que[++tail]=p;
for(int i=fir[p];i;i=e[i].nxt)
if(e[i].to!=fa) dfs1(e[i].to,p);
}
inline void dfs2(int p,int fa)
{
for(int i=0;i<=k;i++) s1[i]=s2[i]=cnt[i]=0;
dep[p]=tail=0,dfs1(fa,p);
for(int i=1;i<=tail;i++) s1[dep[que[i]]]++;
for(int i=1;i<=k;i++) s1[i]+=s1[i-1];
f[p]=1,g[p]=s1[k];
for(int i=fir[p];i;i=e[i].nxt)
{
int q=e[i].to;
if(q==fa) continue;
tail=0,dfs1(q,p);
for(int j=1;j<=tail;j++)
{
if(dep[que[j]]>k) continue;
cnt[dep[que[j]]]++;
g[p]+=s1[k-dep[que[j]]];
f[p]+=s2[k-dep[que[j]]]+1;
}
for(int j=1;j<=k;j++) s2[j]=s2[j-1]+cnt[j];
}
for(int i=fir[p];i;i=e[i].nxt)
if(e[i].to!=fa) dfs2(e[i].to,p);
}
inline void dfs3(int p,int fa)
{
size[p]=1;
for(int i=fir[p];i;i=e[i].nxt)
{
int q=e[i].to;
if(q==fa) continue;
dfs3(q,p);
size[p]+=size[q];
f[p]-=cal(size[q]);
}
f[p]+=cal(size[p]);
g[p]=1ll*size[p]*(n-size[p]);
}
inline void solve()
{
if(flag)
{
for(int i=1;i<=n;i++)
{
f[i]=min(n-i+1,k+1);
pre[i]=pre[i-1]+f[i]-1;
int last=max(1,i-k);
ll add=1ll*(i-last-1)*(i-last)/2;
g[i]=pre[i-1]-pre[last-1]-add;
}
return;
}
if(k==0)
{
for(int i=1;i<=n;i++) f[i]=1,g[i]=0;
return;
}
if(k==n-1) dfs3(1,0);
else dfs2(1,0);
}
int main()
{
int a,b; ll ans1=0,ans2=0;
scanf("%d%d%d",&n,&m,&k);
for(int i=1;i<n;i++)
{
scanf("%d%d",&a,&b);
add(a,b);
if(a!=b-1) flag=0;
}
solve();
for(int i=1;i<=n;i++)
{
f[i]%=mod,g[i]%=mod;
ans1=(ans1+f[i])%mod;
ans2=(ans2+power(f[i]+g[i],m)-power(g[i],m)+mod)%mod;
}
printf("%lld\n",((power(ans1,m)-ans2)%mod+mod)%mod);
return 0;
}
题目都不错的,我真良心