题解
题解就随便一点了,能看懂就行了
题目皆为原题,非原创,下文会标注原题出处
这次如果大家都不挂分的话应该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即可得出答案
但是问题是,我们应该一直找到多少钱的方案才能保证找到合法的最优解呢?我认为这是这道题最关键的部分,以下为证明:
$ ext { 关于这里的 } m x, ext { 我们令它为 } max {i=1}^{n} v{i}^{2} $
假设, 在 (m+m x) 的范围内, 没有(买家付的钱(-)卖家找的钱等于 (m) )的情况, 那么卖家所找的硬币的个数必定大于 (v_{max } quadleft( ight.) 因为 (v_{max }^{2}) 显然小于 (left.m x ight)) 。设卖家找的钱的序列为 (pleft(forall p_{i}<p_{i+1} ight)) 我们作一个前缀数组 (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,) 因此可以 (Oleft(2^{n} ight)) 去枚举所有可能的串, 然后对于每个询问 (O(m)) 的逐个判定是否合 法。时间复杂度 (Oleft(q m 2^{n} ight))。
子任务 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} ight) &left(a_{2} oplus b_{2} ight) eq 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}inom{m}{i}g[p]^i imes 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)=inom{x}{2}+x),表示在(x)个点中选出(2)个点满足终点不小于起点
(displaystyle f[p]=cal(size[p])-sum_{qin son(p)}cal(size[q])quad g[p]=(n-size[p]) imes 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}inom{m}{i}g[p]^if[p]^{m-i}-g[p]^m=(f[p]+g[p])^m-g[p]^m)
这样就可以(O(nlog m))计算了,然后就没了
但不妨从容斥的角度直接得到最后的答案
显然这(m)条路径全部相交的点,肯定是形成一条链(边形成的),这条链的长度为(x)
设(f(x))表示这条链的长度为(x)的方案数,(g(x))表示钦定这条链的长度为(x)的方案数,那么就有:
(displaystyle g(x)=sum_{dgeq x}(d-x+1)f(d)),直接展开得到:
(g(0)=f(0)+2 imes f(1)+3 imes f(2)+...)
(g(1)=1 imes f(1)+2 imes 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
",((power(ans1,m)-ans2)%mod+mod)%mod);
return 0;
}
题目都不错的,我真良心