做题记录 25~26.1.09
2025
CF2165C Binary Wine
贪心、观察性质
设 \(x_i\) 表示二进制数 \(x\) 的第 \(i\) 位。
假设 \(a\) 已经固定,从高位往低位贪心,现在在第 \(i\) 位,若:
- \(\sum\limits_{j=1}^n a_{j,i}>c_i\),任取一个 \(j\) 满足 \(a_{j,i}=c_i\),后面就都可以任意填了,一定合法。
- \(\sum\limits_{j=1}^n a_{j,i}<c_i\),一定不合法,需要调整。
- \(\sum\limits_{j=1}^n a_{j,i}=c_i\),需要继续往后考虑。
注意到调整一定不会使 \(\sum\limits_{j=1}^n a_{j,i}>c_i\),因为可以把调多的 \(1\) 放到低位,一定仍能构造且答案不会变大。
每次遇到 \(2\) 情况直接找到 \(a_j\mod{2^{i+1}}\) 最大的 \(j\),调成第 \(i\) 位为 \(1\),低位都为 \(0\) 即可,可以证明这样是不劣的。
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
typedef pair<int,int> pii;
template<typename T>
void chkmin(T &x,const T &y){x=min(x,y);}
template<typename T>
void chkmax(T &x,const T &y){x=max(x,y);}
const int N=500005,Lg=35;
int n,q,a[N],sum[N],d[N],vis[N];
vector<pii> vec[Lg];
void __INIT__(){}
void __SOLVE__(){
scanf("%d%d",&n,&q);
for(int j=0;j<30;j++) vec[j].clear(),sum[j]=0;
for(int i=1;i<=n;i++){
scanf("%d",&a[i]);
for(int j=0;j<30;j++) vec[j].push_back({a[i]&((1<<(j+1))-1),i}),sum[j]+=((a[i]>>j)&1);
}
for(int j=0;j<30;j++) sort(vec[j].begin(),vec[j].end(),greater<pii>());
for(int i=1;i<=n;i++) vis[i]=0;
while(q--){
int c,ans=0;
scanf("%d",&c);
vector<int> vised;
for(int j=29;j>=0;j--){
if(sum[j]>((c>>j)&1)) break;
if(sum[j]==((c>>j)&1)) continue;
int i,x;
for(i=0;i<n&&vis[vec[j][i].second];i++);
if(i<n) x=vec[j][i].first,i=vec[j][i].second;
else i=1,x=0;
// printf("j=%d %d %d\n",j,i,x);
vis[i]=1,vised.push_back(i);
ans+=(1<<j)-(x&((1<<(j+1))-1));
for(int k=0;k<j;k++) sum[k]-=((x>>k)&1),d[k]-=((x>>k)&1);
}
printf("%d\n",ans);
for(int j=0;j<30;j++) sum[j]-=d[j],d[j]=0;
for(auto i:vised) vis[i]=0;
}
}
int main(){
int T;
scanf("%d",&T);
__INIT__();
while(T--) __SOLVE__();
return 0;
}
CF2165D Path Split
网络流、动态规划、贪心
不难想到建模为最小连覆盖,转为 \(n\) - 拆点二分图上最大匹配。每个点 \(i\) 向满足 \(j>i\) 且 \(|a_i-a_j|=1\) 的 \(j\) 连边,建拆点二分图。关键在于想到使用 Hall 定理,把值域分奇偶 \(\mathcal{O}(\sum cnt_i)\) 做一下优化 DP 即可。
还有贪心做法,从小到大扫值域。把匹配分成向下匹配和向上匹配两种,向下匹配即左部点值大于右部点,反之亦然。对于向上匹配,注意到匹配到靠前的数一定不劣,所以对于值同为 \(v\) 的一系列点,从左往右扫,贪心匹配右侧最靠近的 \(v+1\)。向下匹配反过来即可。
CF2165E Rainbow Branch
树上问题、贪心
首先我们会 \(k=1,2,n-1\)。
考虑 \(k\) 固定怎么做。
观察样例,发现如果 \(\exists u,deg_u\ge k\),\(ans_k\le 2\)。
继续考虑 \(ans_k\le 3\) 的限制,首先想到把叶子染成不同颜色,如果 \(|leaf|+1\ge k\),那么 \(ans_k\le 3\)。更进一步,发现如果存在一个连通子图叶子数量 \(\ge k\),那么 \(ans_k\le 3\)(其实没必要有这步)。
更进一步,考虑构造一定长成一堆连通子图嵌套的形式(即把树分成若干个连通块)。
后面不会了,看了题解。
我们仍考虑 \(ans_k\le 3\) 的限制,注意到连通子图其实一定是整个图,因为轮廓线越深长度越大(我们称一个连通子图的叶子集合为一个轮廓线)。因此一层一层染叶子就是对的,不过删叶子可能会导致剩余的树不连通,但这其实没影响。
一棵树的叶子数量居然大于等于每个点度数的最大值,太神奇了
P10008 [集训队互测 2022] Range Minimum Element
好题,虽然没切掉,题解。
- 对一个不好处理的对象计数,考虑给这个对象构造一个映射使其变得好处理。(这题不是求和,所以显然不能拆贡献)
- 对于简单加乘形式的转移,可以考虑 Lagrange 插值,注意 \(I_{l,r} \in {0,1}\)。(证明答案是多项式可以考虑数学归纳,以及自然数幂求和重要结论,一个多项式值域上的前缀和是次数加一的多项式)
P7324 [WC2021] 表达式求值
想到了拆贡献,没想到后面怎么做……
拆贡献,试求 \(A_{i,j}\) 成为答案的方案数(若 \(A_{i,k}=A_{j,k}\),钦定第一维小的更小),注意到对每个 \(A_{i,j}\) 都算一遍时间复杂度至少是 \(\mathcal{O}(n|E|)\),肯定过不去。注意到 \(m\) 很小,进一步发现我们只关心哪些数比 \(A_{i,j}\) 小,因此只需要对 \(2^m\) 种大小关系分别算一遍即可。
AT_arc207_a [ARC207A] Affinity for Artifacts
初始令每个灯都用最大的代价点亮,DP 求代价在 \(X\) 以内的方案数,即要求
这里的 \(a\) 是已经确定了顺序的。右侧是个常数,不用管它具体是什么。去掉一些边界条件,这样值域被限制在 \(\mathcal{O}(n)\),右侧被限制在 \(\mathcal{O}(n^2)\)。
我们可能填 \(\le i-1\) 的数,也有可能填 \(>i-1\) 的数,发现如果立即处理 \(>i-1\) 的数有后效性,不好处理,于是贡献延迟计算。设 \(f_{i,j,k}\) 表示填了前 \(i\) 个位置,有 \(j\) 个位置贡献被延迟计算,不等式左侧 \(=k\)。状态数 \(\mathcal{O}(n^4)\),暴力枚举之前填了多少个 \(i-1\) 转移看似 \(\mathcal{O}(n)\),实际上因为 \(\sum cnt_i=n\),所以转移复杂度均摊 \(\mathcal{O}(1)\)。
P6961 [NEERC 2017] Journey from Petersburg to Moscow
好题。
不难想到从大到小加边(即钦定第 \(k\) 大),跑最短路。如果一条路径算了 \(\ge k\) 条边,肯定不会导致答案变小。唯独如果只算了 \(\le k\) 条边,答案会边小。
我们试图计算恰好 \(k\) 条边,于是试图强行补回来 \(k\cdot w_0\)(其中 \(w_0\) 是第 \(k\) 大),于是让所有 \(<w_0\) 的边边权变为 \(0\),让 \(\ge w_0\) 的边边权 \(-w_0\),跑最短路,然后加回来 \(k\cdot w_0\)。
考虑这样的结果,如果算了 \(>k\) 条边,把加回来的 \(k\cdot w_0\) 视作把最大的 \(k\) 条边边权补回来,然而我们多走了一些边,这些边有非负的边权,故不如走最大 \(k\) 条边。如果算了 \(<k\) 条边,那么本来比 \(w_0\) 小的一些边贡献变成了 \(w_0\),故不如走恰好 \(k\) 条边。
P11880 [RMI 2024] 选区间 / Choose Interval
神秘观察性质,根本做不出来……
两种操作很乱斗,不难想到拆全值,默认是一种操作,改成另一种操作会产生一些贡献。本题是最小化问题,所以先加后减的性质比先减后加优,考虑现另所有区间都选 \(1\) 类操作,改成 \(2\) 类操作时,会全局 \(+1\),同时区间内 \(-2\)。考虑二分答案 \(mid\),再枚举改了 \(k\) 个区间,然后贪心即可做到 \(\mathcal{O}(n^2\log^2n)\)。
优化……注意到最优的 \(k\) 只会在 \(max-mid\) 或 \(max-mid+1\) 取到。
证明(引用本文):
- 证明下界。每多选一个区间最大值至多 \(-1\),故至少需要 \(max-mid\) 个区间才能使最大值 \(\le mid\)。
- 证明上界。只需证明 \(\forall k\ge max-mid\),若 \(k+2\) 合法,则 \(k\) 也合法(若本命题成立,则利用其逆否命题即可证明原结论)。
- 若这 \(k+2\) 个区间中,有两个区间无交。删去这两个区间,则不被覆盖这两个区间的点会从 \(x\) 变为 \(x-2\);被这两个区间覆盖的点数值不会变化。最大值不会变大。
- 若这 \(k+2\) 个区间两两相交,则找到两个区间使其交为所有区间的交。不在交中的点不会变大,考虑在交中的点,若输入时数值是 \(x_0\),则考虑这 \(k+2\) 个区间时数值是 \(x_0-k-2\),删去选中的两个区间后数值是 \(x_0-k\)。若删去选中的区间后不合法,则 \(x_0-k>mid\),又因为 \(k\ge max-mid\),得 \(x_0>max\),矛盾,因此删去选中的区间后仍合法。
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
typedef pair<int,int> pii;
typedef pair<int,ll> pil;
typedef pair<ll,int> pli;
typedef pair<ll,ll> pll;
template<typename T>
void chkmin(T &x,const T &y){x=x<y?x:y;}
template<typename T>
void chkmax(T &x,const T &y){x=x>y?x:y;}
const int N=200005;
int n,a[N<<1],ans[N];
struct Seg{
int l,r,id;
} seg[N];
bool cmp(Seg x,Seg y){
return x.l<y.l;
}
bool check(int mid,int k){
// printf("check %d %d\n",mid,k);
if(k>mid) return false;
for(int i=1;i<=n;i++) ans[i]=1;
for(int i=1;i<=(n<<1)+1;i++) a[i]=0;
for(int i=1;i<=n;i++) a[seg[i].l]++,a[seg[i].r+1]--;
mid-=k;
priority_queue<pii> pq;
for(int i=1,j=1,s=0;i<=(n<<1)+1;i++){
s+=a[i];
// printf("%d->",s);
for(;j<=n&&seg[j].l<=i;j++) pq.push({seg[j].r,seg[j].id});
while(s>mid&&k){
if(pq.empty()||pq.top().first<i) return false;
a[pq.top().first+1]+=2,s-=2,ans[pq.top().second]=0;
pq.pop(),k--;
}
if(s>mid) return false;
// printf("%d\n",s);
}
return true;
}
int main(){
scanf("%d",&n);
for(int i=1;i<=n;i++) scanf("%d%d",&seg[i].l,&seg[i].r),seg[i].id=i;
sort(seg+1,seg+n+1,cmp);
int l=0,r=n,mx=0;
for(int i=1;i<=n;i++) a[seg[i].l]++,a[seg[i].r+1]--;
for(int i=1;i<=(n<<1)+1;i++) a[i]+=a[i-1],chkmax(mx,a[i]);
while(l<r){
int mid=(l+r)>>1;
if(check(mid,mx-mid)||check(mid,mx-mid+1)) r=mid;
else l=mid+1;
}
check(l,mx-l)||check(l,mx-l+1);
printf("%d\n",l);
for(int i=1;i<=n;i++) printf("%d",ans[i]);
printf("\n");
return 0;
}
P11646 【MX-X8-T5】「TAOI-3」蓝宝石的存在证明
每个一度点显然要与其指向的点划分到一个集合。后面不会了,看题解后发现:
- 不存在不能被划分的图:考虑拉出任意生成树,不断删叶子,如果根孤立,就随便连到一个邻居。
- 划分唯一当且仅当每个非一度点都有至少一个一度点相邻。这其实可以在证结论 1 的时候感受出来,如果一个非一度点没有相邻的一度点,它就可以随便找一个邻居连,至少有两种方法。
枚举非一度点数量,推推式子就做完了。
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
typedef pair<int,int> pii;
typedef pair<int,ll> pil;
typedef pair<ll,int> pli;
typedef pair<ll,ll> pll;
template<typename T>
void chkmin(T &x,const T &y){x=x<y?x:y;}
template<typename T>
void chkmax(T &x,const T &y){x=x>y?x:y;}
int MOD;
void add(int &x,int y){
x+=y;
if(x>=MOD) x-=MOD;
}
int qpow(int a,int b){
int mul=1;
while(b){
if(b&1) mul=(ll)mul*a%MOD;
a=(ll)a*a%MOD,b>>=1;
}
return mul;
}
const int N=5005;
int n,t,C[N][N],str[N][N],fact[N],f[N],pw[N*N];
void __INIT__(){
scanf("%d%d",&MOD,&t);
C[0][0]=1;
for(int i=1;i<N;i++){
C[i][0]=1;
for(int j=1;j<=i;j++) C[i][j]=(C[i-1][j-1]+C[i-1][j])%MOD;
}
str[0][0]=1;
for(int i=1;i<N;i++) for(int j=1;j<=i;j++) str[i][j]=((ll)j*str[i-1][j]%MOD+str[i-1][j-1])%MOD;
fact[0]=1;
for(int i=1;i<N;i++) fact[i]=(ll)i*fact[i-1]%MOD;
pw[0]=1;
for(int i=1;i<N*N;i++) pw[i]=(pw[i-1]<<1)%MOD;
if(t){
f[1]=1%MOD;
for(int i=2;i<N;i++){
f[i]=pw[i*(i-1)>>1];
for(int j=1;j<i;j++) add(f[i],MOD-(ll)C[i-1][j-1]*f[j]%MOD*pw[(i-j)*(i-j-1)>>1]%MOD);
}
}
else{
f[1]=1%MOD;
for(int i=2;i<N;i++) f[i]=qpow(i,i-2);
}
}
void __SOLVE__(){
scanf("%d",&n);
if(n<=3){
int tmp[3]={0,1,t?4:3};
printf("%d\n",tmp[n-1]%MOD);
return;
}
int ans=0;
for(int i=1;i<=n;i++) add(ans,(ll)f[i]*str[n-i][i]%MOD*C[n][i]%MOD*fact[i]%MOD);
printf("%d\n",ans);
}
int main(){
int T;
scanf("%d",&T);
__INIT__();
while(T--) __SOLVE__();
return 0;
}
AT_arc146_d [ARC146D] >=<
原限制相等很麻烦,不难想到拆限制,将限制改为:
- \(A_{P_i}\le X_i\land A_{Q_i}\le Y_i\) 或 \(A_{P_i}>X_i\land A_{Q_i}>Y_i\)
- \(A_{P_i}\le X_i-1\land A_{Q_i}\le Y_i-1\) 或 \(A_{P_i}>X_i-1\land A_{Q_i}>Y_i-1\)
直接调整即可,初始令所有点都为 \(1\),若有 \(A_{P_i}\le 0\land A_{Q_i}\le Y\) 或 \(A_{P_i}>0\land A_{Q_i}>Y\) 的限制,会让 \(A_{Q_i}>Y\) 这又会引发一些限制的变化,递归处理,通过题解得知可以用 2-sat 证明正确性。
P7515 [省选联考 2021 A 卷] 矩阵游戏
这是一个解方程的问题,会有主元和自由元,考虑设第一行和第一列的数为自由元,则其他数都能用这些数表示出来,即
不难想到先钦定第一行、第一列为 \(0\),然后调进限制区间中。一个自然的想法是,对着上式解不等式,但是现在每一个不等式都有三个未知数,并不好做。
看题解后发现,可以将自由元也调整。具体地,设第 \(i\) 行调整量为 \(r_i\),第 \(j\) 列调整量为 \(c_j\),则最终矩阵
这样每个不等式都是二元的,我们想要其变为差分约束,考虑把和转为差。将 \(r\) 的奇数位取反,\(c\) 的偶数位取反,就可以跑差分约束了。
P11520 [THUPC 2025 初赛] 骑行计划
借用连续段 dp 的思想,利用不优性。将骑行卡按 \(t_i\) 从大到小排序,设 \(f_{i,l,r}\) 表示用前 \(i\) 个覆盖了 \([l,r]\) 所需最小代价。转移考虑第 \(i\) 个区间第一次出现的位置即可。
AT_agc021_d [AGC021D] Reversed LCS
正串反串相关,考虑一些和回文相关的东西,(看题解后)注意到 正串反串的 \(LCS\) 等于正串的最长回文子序列,证明。
设 \(f_{l,r,k}\) 表示区间 \([l,r]\) 改了 \(k\) 个字符的最长回文子序列,转移考虑左右端点是否匹配即可。
P11457 [USACO24DEC] Job Completion G / P4053 [JSOI2007] 建筑抢修
典题,不会:(。
(以 P11457 为背景)
有一个自然的想法是,按紧迫度排序然后贪心。但如果按 \(s_i\) 排序则很容易构造反例,先做 \(s_i\) 较大但耗时较短的可能更优。考虑按 \(s_i+t_i\) 排序。
证明
感性理解,看着就挺对的考虑邻项交换,设相邻两项 \(i,j\) 满足若先完成 \(i\) 则 \(j\) 无法完成,反过来则都能完成。设起始时间为 \(T\),可得不等式组:
\[\begin{cases} T+t_i>s_j \\ T+t_j\le s_i \end{cases} \]如果我们能把左侧化成相同形式,就可以比较了,考虑给 \(1\) 式 \(+t_j\),给 \(2\) 式 \(+t_i\),得 \(s_j+t_j< T+t_i+t_j\le s_i+t_i\)。
因此按 \(s_i+t_i\) 排序是对的。
从左往右扫,用大根堆维护选中的区间,如果新加入的区间无法完成,把(包括当前区间)最长的删去即可。
P14473 [集训队互测 2025] 少年汹涌
先考虑 \(n=1\) 怎么做。注意到步长序列 \(d_x=w_{popcount(x)}\),其具有倍增结构,考虑倍增。设 \(f_{i,j,k}\) 表示长 \(2^i\) 的序列 \(d'_x=w_{popcount(x)+j}\),从 \(k\) 开始跳(\(k\) 的范围与 \(w\) 范围相同),跳出 \(2^i\) 需要多少步以及多走了多少步,这是容易预处理的。考虑从 \(a\) 跳到 \(L\),每次跳一个长为 \(lowbit(a)\) 的区间,即可 \(\mathcal{O}(\log V)\) 处理。
对于 \(n>1\) 的情况,考虑所有点走过的路径形如一颗树,用 \(priority\_queue\) 维护所有叶子,每次去掉一个叶子(即类似拓扑排序)。删叶子肯定也需要倍增,大体过程与 \(n=1\) 类似,但是跳到 \(LCA\) 处要特殊处理,因为 \(LCA\) 可能在该区间的一个神秘位置,我们需要找到这个位置。考虑类似线段树上二分,先 \(\mathcal{O}(1)\) 判断 \(LCA\) 是否在左侧,然后单侧递归。其实就是想办法建虚树。
只能说不愧是集训队互测,太乱了,代码是根本不可能写出来的……
CF1458D Flip and Reverse
好题,好难……
不难想到前缀和,一个区间可翻转当且仅当 \(s_{l-1}=s_r\),建图,\(n\) 个点表示 \(s\) 的 \(n\) 个取值,把 \(s_i\) 向 \(s_{i+1}\) 连边,注意到操作一个子区间等价于翻转一个环。
题解告诉我们关键结论:本题中,我们可以从一个欧拉路径出发,只通过翻转环生成所有欧拉路径。
证明(引用本文)
先发现一个前置结论:翻转环不会导致边集发生变化。因为所有边都只连接相邻的点,而且选中的部分是环,所以 \(u\) 与 \(u+1\) 的间隔从左往右经过次数与从右往左经过次数是相等的,因此边集不变。
有这个结论之后,我们考虑证明两个欧拉路径 \(A,B\) 可以相互转化。考虑两个路径第一个分叉点 \(u\)(起点都是 \(0\),不会有 corner case),不妨设 \(A\) 从 \(u\) 往 \(u-1\) 走,\(B\) 从 \(u\) 往 \(u+1\) 走。因为 \(u\) 还有一个出度没走,所以 \(u\) 肯定还有一个入度没走,若这个入度是 \(u-1\rightarrow u\),翻转 \(B\),否则翻转 \(A\),最终总能变为相同的路径。因为边集不变,所以翻转可逆,我们在得到上述调整方案之后可以继续调整为将 \(A\) 调整成 \(B\)。
求字典序最小的欧拉回路即可。
CF1583H Omkar and Tours
考虑按容量从小到大加边,处理所有限制等于当前最大边权的查询,这样就能保证容量限制。
对每个连通块,考虑建出点权最大的那些点构成的虚数,则答案是虚树上最大边权和起点到虚树路径上的最大边权取 \(\max\)。合并两个连通块的时候,如果两边最大点权不同,直接保留一侧;否则可以用 LCT 维护。
LCT 很难写,考虑更简单的办法,注意到每条边不一定要统计恰好一次,所以我们可以把起点到虚数路径上的最大边权改为起点到任意关键点的最大边权,于是考虑对每个虚树只保留任意一个作为代表元以及最大边权。合并两个虚树的时候,只需要把代表元之间的最大边权更新到现有的虚数最大边权上即可。
2026.1
ARC132E Paw
反套路题,当你想把期望拆开对每一个格子单独考虑时,就完了。
手玩一下,注意到答案一定形如 <<...<<=>>...>> 其中 = 表示从原序列保留的恰一个间隔。
证明:
归纳证明。考虑最后一次操作的格子(走过的洞),若向左走,则右侧为子问题,反之亦然。
接下来考虑把 \(i\) 个 . 全部变成向左足迹且不影响右边的概率。考虑第一步的可能情况,发现只要不选择最后一个 . 且向右走即可,概率为 \(1-\frac{1}{2i}\),删去这个点后是子问题,所以有转移:
右侧是对称的,不需要重新计算。设一共有 \(k\) 个 .,第 \(i\) 个 . 为 \(p_i\)(特别地,\(p_0=0,p_{k+1}=n+1\)),原序列子区间 \([l,r]\) 上 \(1\) 个数为 \(cnt_{l,r}\),答案为
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
typedef pair<int,int> pii;
typedef pair<int,ll> pil;
typedef pair<ll,int> pli;
typedef pair<ll,ll> pll;
template<typename T>
void chkmin(T &x,const T &y){x=min(x,y);}
template<typename T>
void chkmax(T &x,const T &y){x=max(x,y);}
const int MOD=998244353;
void add(int &x,int y){
x+=y;
if(x>=MOD) x-=MOD;
}
int qpow(int a,int b){
int mul=1;
while(b){
if(b&1) mul=(ll)mul*a%MOD;
a=(ll)a*a%MOD;
b>>=1;
}
return mul;
}
const int N=200005;
int fact[N],invfact[N],inv[N];
void init(){
fact[0]=1;
for(int i=1;i<N;i++) fact[i]=(ll)i*fact[i-1]%MOD;
invfact[N-1]=qpow(fact[N-1],MOD-2);
for(int i=N-1;i>0;i--) invfact[i-1]=(ll)i*invfact[i]%MOD;
for(int i=1;i<N;i++) inv[i]=(ll)invfact[i]*fact[i-1]%MOD;
}
int n,f[N],sum[N];
char s[N];
int main(){
init();
scanf("%d",&n);
scanf("%s",s+1);
f[0]=1;
for(int i=1;i<=n;i++) f[i]=(ll)f[i-1]*(MOD+1-inv[i<<1])%MOD;
int cnt=0,k=0;
for(int i=1;i<=n;i++) k+=(s[i]=='.'),sum[i]=sum[i-1]+(s[i]=='<');
int lst=0,ans=0;
for(int i=1;i<=n+1;i++){
if(i==n+1||s[i]=='.'){
add(ans,(ll)f[cnt]*f[k-cnt]%MOD*(lst+sum[i-1]-sum[lst])%MOD);
cnt++,lst=i;
}
}
printf("%d\n",ans);
return 0;
}
P9479 [NOI2023] 桂花树
注意不要转化错题意……
原来的限制很抽象,考虑转化。第一个限制等价于 \(1\sim n\) 在 \(T'\) 上的虚树等于 \(T\)(如果已知所有 \(LCA(u,v)\) 能唯一确定一颗有根树)。对于另外一个限制,只需考虑该点作为最大值的情况,即对于结点 \(i\),只保留 \(1\sim i\) 的虚树,则 \(i\) 到根祖先所有结点编号 \(\le i+k\)。
先考虑 \(k=0\) 的情况。此时有更强的结论,\(1\sim i\) 的虚树只包含 \(1\sim i\) 的点。从小到大加点,则加 \(i\) 时自然只关注了 \(1\sim i-1\) 的虚树,此时 \(i\) 只能加在一条边上或挂在一个点上。答案为
再考虑一般情况。我们仍从小到大加数,此时,加入 \(i\) 时还可能再加入一个点(具体形态可参考动态虚树),这个点的编号应 \(\in[i+1,i+k]\),延迟确定这个点的标号。考虑 DP,设 \(f_{i,S}\) 表示确定了 \(1\sim i\),未确定标号的点上界集合为 \(S\)(显然是不重集,因为每次最多加一个空点,且相同的 \(i+k\) 不可能对应不同的 \(i\))。转移:
- \(i+1\notin S\),\(f_{i+1,S}\leftarrow (2(i+|S|)-1)f_{i,S}\)
- \(i+1\notin S\),\(f_{i+1,S\cap\{i+k+1\}}\leftarrow (i+|S|-1)f_{i,S}\)
- \(f_{i+1,S/\{j\}}\leftarrow f_{i,S}\)(若 \(i+1\in S\),\(j=i+1\),否则 \(j\) 遍历 \(S\))
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
typedef pair<int,int> pii;
typedef pair<int,ll> pil;
typedef pair<ll,int> pli;
typedef pair<ll,ll> pll;
template<typename T>
void chkmin(T &x,const T &y){x=min(x,y);}
template<typename T>
void chkmax(T &x,const T &y){x=max(x,y);}
const int MOD=1000000007;
void add(int &x,int y){
x+=y;
if(x>=MOD) x-=MOD;
}
int qpow(int a,ll b){
int mul=1;
while(b){
if(b&1) mul=(ll)mul*a%MOD;
a=(ll)a*a%MOD;
b>>=1;
}
return mul;
}
const int N=33005;
int n,m,k,f[2][1<<15];
#define lowbit(x) ((x)&(-(x)))
void __INIT__(){}
void __SOLVE__(){
scanf("%d%d%d",&n,&m,&k);
for(int i=2;i<=n;i++) scanf("%*d");
for(int i=0;i<(1<<k);i++) f[0][i]=f[1][i]=0;
f[n&1][0]=1;
for(int i=n;i<=n+m-1;i++){
int t=(i&1);
for(int s=0;s<(1<<k);s++){
int sz=i+__builtin_popcount(s);
if(!(s&1)){
add(f[t^1][s>>1],(ll)(2*sz-1)*f[t][s]%MOD);
if(k) add(f[t^1][(s>>1)|(1<<(k-1))],(ll)(sz-1)*f[t][s]%MOD);
for(int x=(s>>1);x;x^=lowbit(x)) add(f[t^1][(s>>1)^lowbit(x)],f[t][s]);
}
else add(f[t^1][s>>1],f[t][s]);
f[t][s]=0;
}
}
printf("%d\n",f[(n+m)&1][0]);
}
int main(){
int T=1;
scanf("%*d%d",&T);
__INIT__();
while(T--) __SOLVE__();
return 0;
}
P14941 「FAOI-R10」梦
什么究极分讨题……
考虑枚举 \(c\),再按字典序从小到大枚举 \(s_c\) 的后缀 \(x\),判断其互补的前缀是否出现过(用 trie 预处理一下),分讨 \(s_b\) 的两种情况:
\(s_b\le x\)。预处理每个字符串在 trie 上的子树权值和(即可能的 \(d\) 数量),再前缀和一下,就可以求 \(rk_{s_b}\in[l,r]\) 的 \(d\) 数量。因为还有 \(a,b,c,d\) 互不相同的限制,所以要容斥一下去重:
- 减去 \(b=a\)。这个直接算即可。
- 减去 \(d=a\)。这个限制了 \(s_b\) 是 \(s_a\) 的前缀,再分讨一下
- \(s_b\) 是 \(lcp(s_a,x)\) 前缀,\(b\) 在此条件下随便取,直接计数。
- 其他情况,这个有贡献当且仅当 \(|lcp(s_a,x)|<|s_a|\) 且 \(s_a<x\),\(b\) 在此条件下随便取,直接计数。
- 减去 \(d=c\)。与上面类似。
- 加上 \(b=a,d=c\)。直接判。
\(s_b>x\)。这种情况要求 \(x\) 是 \(s_b\) 前缀。改变枚举顺序,枚举 \(d\),则 \(x\) 是 \(s_d\) 前缀,开一个桶记录 \(x\) 对应了多少 \((a,c)\),再考虑 \(b\)。(后文定义 \(s-t\) 表示 \(s\) 去掉 \(t\) 的前缀后的剩余部分)则 \(s_b-x\) 是 \(s_d-x\) 的前缀,且 \(s_b-x<s_d\)。与上面去重时操作类似:先算 \(s_b-x\) 是 \(lcp(s_d,s_d-x)\) 前缀的部分,这个直接枚举并判断;再算其他情况,先判断 \(|lcp(s_d-x,s_d)|<|s_d-x|\) 且 \(s_d-x<s_d\),然后枚举并判断。去重也是与上面类似的。
upd:
\(s_b>x\) 的情况有更简单做法。注意到 \(|s_b|>|b|-|x|\),所以枚举 \(b\),再枚举 \(s_b\) 的后缀,还是用一个桶求 \((a,c)\) 数量,字典序关系可直接判断。
ARC132F Takahashi The Strongest
想到 FWT 之后,后面就简单了(虽然笔者被提示后才想到 FWT)。
Takahashi 的必要条件为另外两个人策略相同。考虑对于 \(a_i,b_j\) 保留其重复位置,考虑 \(4\) 进制 FWT,位运算为
| \(\circ\) | 0 | 1 | 2 | 3 |
|---|---|---|---|---|
| 0 | 0 | 3 | 3 | 3 |
| 1 | 3 | 1 | 3 | 3 |
| 2 | 3 | 3 | 2 | 3 |
| 3 | 3 | 3 | 3 | 3 |
根据另一个角度的FWT,尝试构造对应可逆矩阵,得到
逆矩阵为
注意到至少一次获胜不好直接计算,正难则反,计算不获胜的方案数。不获胜即不能从 \((i+1)\mod 3\) 转移到 \(i\),考虑构造 FWT 矩阵:
输出的时候拿 \(nm\) 减一下即可,时间复杂度 \(\mathcal{O}(4^kk)\)。
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int K=15;
void fwt1(ll *f,int n,int type){
for(int w=1;w<n;w<<=2){
for(int i=0;i<n;i+=(w<<2)){
for(int j=0;j<w;j++) f[i+j+w*3]+=type*(f[i+j]+f[i+j+w]+f[i+j+w*2]);
}
}
}
void fwt2(ll *f,int n){
for(int w=1;w<n;w<<=2){
for(int i=0;i<n;i+=(w<<2)){
for(int j=0;j<w;j++){
ll sum=f[i+j]+f[i+j+w]+f[i+j+w*2]+f[i+j+w*3];
ll a=sum-f[i+j+w],b=sum-f[i+j+w*2],c=sum-f[i+j];
f[i+j]=a,f[i+j+w]=b,f[i+j+w*2]=c,f[i+j+w*3]=0;
}
}
}
}
int k,n,m;
ll f[1<<24],g[1<<24];
char s[K];
int c2i(char c){
if(c=='P') return 0;
if(c=='R') return 1;
if(c=='S') return 2;
exit(-1);
}
int main(){
scanf("%d%d%d",&k,&n,&m);
for(int i=1;i<=n;i++){
scanf("%s",s);
int x=0;
for(int j=0;j<k;j++) x+=(c2i(s[k-j-1])<<(2*j));
f[x]++;
}
for(int i=1;i<=m;i++){
scanf("%s",s);
int x=0;
for(int j=0;j<k;j++) x+=(c2i(s[k-j-1])<<(2*j));
g[x]++;
}
fwt1(f,1<<(2*k),1),fwt1(g,1<<(2*k),1);
for(int i=0;i<(1<<(2*k));i++) f[i]*=g[i];
fwt1(f,1<<(2*k),-1);
fwt2(f,1<<(2*k));
int tmp=1;
for(int i=1;i<=k;i++) tmp*=3;
for(int i=0;i<tmp;i++){
int y=0;
for(int j=0,x=i;x;j++,x/=3) y+=((x%3)<<(2*j));
printf("%lld\n",(ll)n*m-f[y]);
}
return 0;
}
ABC225H Social Distance 2
组合计数还是太菜了……直接用 Bostan-Mori 硬做的。其他做法去看别人的题解吧。
先不管人的顺序,考虑根据座位把序列划分为 \(M-1\) 段(首尾两个不贡献的段不管)。每一段贡献的生成函数为
两个固定点之间是段的组合。设两个相邻固定点 \(A_p,A_{p+1}\) 之间放 \(i\) 段贡献和为 \(f_i\),则生成函数 \(F_p(x)=\sum\limits_{i}f_ix^i\) 为
这个东西大抵是可以写成组合数的,但是我不会,我要 BM。
注意到 BM 每向下递归一层,这个式子里系数作为关于 \(x\) 的多项式次数只会翻一倍。所以递归 \(\log_2(A_{p+1}-A_p)\) 层最终得到 \(F_p(x)\) 的次数是 \(\mathcal{O}(A_{p+1}-A_p)\) 的,一次 BM 时间复杂度 \(\mathcal{O}((A_{p+1}-A_p)\log^2N)\)。
对于一侧固定,另一侧任意的段的组合,只需把系数做前缀和:
对于两侧均不固定(\(K=0\))的情况,做二阶前缀和:
这两种情况 BM 处理类似,求出所有 \(F\) 的时间复杂度为 \(\mathcal{O}(\sum\limits_{p}(A_{p+1}-A_p)\log^2N)=\mathcal{O}(N\log^2N)\)。
接下来,只需要做一个分治 ntt,把所有 \(F\) 卷起来即可。(值得注意的是,根本不需要像某篇题解所述用哈夫曼树结构,因为每一层的复杂度总是 \(\mathcal{O}(N\log N)\),我们只需要保证分治层数为 \(\mathcal{O}(\log N)\),总复杂度就是 \(\mathcal{O}(N\log^2N)\))
答案是
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
typedef pair<int,int> pii;
typedef pair<int,ll> pil;
typedef pair<ll,int> pli;
typedef pair<ll,ll> pll;
typedef vector<int> vi;
template<typename T>
void chkmin(T &x,const T &y){x=min(x,y);}
template<typename T>
void chkmax(T &x,const T &y){x=max(x,y);}
const int inf=0x3f3f3f3f;
const ll infll=0x3f3f3f3f3f3f3f3f;
const int MOD=998244353,G=3;
void add(int &x,int y){
x+=y;
if(x>=MOD) x-=MOD;
}
int qpow(int a,ll b){
int mul=1;
while(b){
if(b&1) mul=(ll)mul*a%MOD;
a=(ll)a*a%MOD;
b>>=1;
}
return mul;
}
int stand(int n){
int len;
for(len=1;len<n;len<<=1);
return len;
}
void ntt(vi &f,bool flag=false){
int n=f.size();
if(n==1) return;
vi rev(n);
for(int i=1;i<n;i++){
rev[i]=rev[i>>1]>>1;
if(i&1) rev[i]|=(n>>1);
}
for(int i=1;i<n;i++) if(rev[i]<i) swap(f[rev[i]],f[i]);
for(int w=1;w<n;w<<=1){
int step=qpow(G,(MOD-1)/(w<<1));
if(flag) step=qpow(step,MOD-2);
for(int i=0;i<n;i+=(w<<1)){
for(int j=0,cur=1;j<w;j++,cur=(ll)cur*step%MOD){
int a=f[i+j],b=(ll)cur*f[i+j+w]%MOD;
f[i+j]=(a+b)%MOD;
f[i+j+w]=(a-b+MOD)%MOD;
}
}
}
if(flag){
int inv=qpow(n,MOD-2);
for(int i=0;i<n;i++) f[i]=(ll)inv*f[i]%MOD;
}
}
vi add(vi f,vi g){
int n=max(f.size(),g.size());
f.resize(n);
for(int i=0;i<g.size();i++) add(f[i],g[i]);
return f;
}
vi sub(vi f,vi g){
int n=max(f.size(),g.size());
f.resize(n);
for(int i=0;i<g.size();i++) add(f[i],MOD-g[i]);
return f;
}
vi mul(int k,vi f){
for(int i=0;i<f.size();i++) f[i]=(ll)k*f[i]%MOD;
return f;
}
vi mul(vi f,vi g){
if(f.size()==1) return mul(f[0],g);
if(g.size()==1) return mul(g[0],f);
int len=f.size()+g.size()-1,n=stand(len);
f.resize(n),g.resize(n);
ntt(f),ntt(g);
for(int i=0;i<n;i++) f[i]=(ll)f[i]*g[i]%MOD;
ntt(f,-1);
f.resize(len);
return f;
}
struct Poly{
vi f[3];
Poly(vi a,vi b,vi c){
f[2]=a,f[1]=b,f[0]=c;
}
vi& operator[](int i){
return f[i];
}
};
vi BM(Poly A,Poly B,ll n){
if(n==0) return A[0];
if(n&1) return BM(
Poly({0},sub(mul(A[1],B[2]),mul(A[2],B[1])),sub(mul(A[1],B[0]),mul(A[0],B[1]))),
Poly(mul(B[2],B[2]),sub(mul(2,mul(B[0],B[2])),mul(B[1],B[1])),mul(B[0],B[0])),
n>>1
);
return BM(
Poly(mul(A[2],B[2]),sub(add(mul(A[0],B[2]),mul(A[2],B[0])),mul(A[1],B[1])),mul(A[0],B[0])),
Poly(mul(B[2],B[2]),sub(mul(2,mul(B[0],B[2])),mul(B[1],B[1])),mul(B[0],B[0])),
n>>1
);
}
const int N=200005;
int n,m,k,a[N];
vi solve(int l,int r){
if(l==r){
if(l==0&&l==k) return BM(
Poly({0},{0},{1}),
Poly({1},{MOD-2,MOD-1},{1}),
n-1
);
if(l==0) return BM(
Poly({0},{MOD-1},{1}),
Poly({1},{MOD-2,MOD-1},{1}),
a[1]-1
);
if(l==k) return BM(
Poly({0},{MOD-1},{1}),
Poly({1},{MOD-2,MOD-1},{1}),
n-a[k]
);
return BM(
Poly({1},{MOD-2},{1}),
Poly({1},{MOD-2,MOD-1},{1}),
a[l+1]-a[l]
);
}
int mid=(l+r)>>1;
return mul(solve(l,mid),solve(mid+1,r));
}
int main(){
scanf("%d%d%d",&n,&m,&k);
for(int i=1;i<=k;i++) scanf("%d",&a[i]);
int ans=solve(0,k)[m-1];
for(int i=1;i<=m-k;i++) ans=(ll)ans*i%MOD;
printf("%d\n",ans);
return 0;
}
P11022 「LAOI-6」Yet Another Graph Coloration Problem
以前做的题突然发现没写记录。当时好像做法假了。
考虑拉出来一个环,不难证明无环一定无解。环会将原图划分为若干子图,每个子图保留任意一条与环相连的边即可。把环上的点交替染色,每个子图的颜色染为其连向的环上的点的颜色。
P10678 『STA - R6』月
不要把最小化读成最大化,笔者因此被卡了半天(虽然最大化也不难做)。
注意到构造过程中不太可能出现一开始有解后面无解的情况。一种比较优的方案是让每个子树的根是该子树的直径中点,则这棵子树的叶子深度不应相差超过 \(1\)。进一步,注意到上面的点度数越大,整棵树越矮。把点的度数从大到小排序,逐层加点即可。
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
typedef pair<int,int> pii;
typedef pair<int,ll> pil;
typedef pair<ll,int> pli;
typedef pair<ll,ll> pll;
template<typename T>
void chkmin(T &x,const T &y){x=min(x,y);}
template<typename T>
void chkmax(T &x,const T &y){x=max(x,y);}
const int inf=0x3f3f3f3f;
const ll infll=0x3f3f3f3f3f3f3f3f;
const int N=200005;
int n;
pii d[N];
void __INIT__(){}
void __SOLVE__(){
scanf("%d",&n);
for(int i=1;i<=n;i++){
scanf("%d",&d[i].first);
d[i].second=i;
}
sort(d+1,d+n+1,greater<pii>());
queue<pii> q;
q.push(d[1]);
for(int i=2;i<=n;){
queue<pii> tmp;
while(q.size()){
int deg=q.front().first,u=q.front().second;
while(deg--){
int v=d[i].second;
printf("%d %d\n",u,v);
d[i].first--,tmp.push(d[i]);
i++;
}
q.pop();
}
q.swap(tmp);
}
}
int main(){
int T=1;
scanf("%d",&T);
__INIT__();
while(T--) __SOLVE__();
return 0;
}
P9870 [NOIP2023] 双序列拓展
不妨假设 \(x_1>y_1\),若不然,则交换两个序列。
手玩一些构造,注意到合法的一个必要条件是 \(\max_x>\max_y\),考虑把 \(x\) 的一个最大值 \(a_p\) 取出来,则 \(\max\) 把序列分成左右两部分,两部分分别覆盖 \(y\) 的前缀后缀即可(前缀后缀交不能 \(>1\))。
先看左侧,考虑贪心。这是一个类似于子问题的东西,考虑把左侧的最大值 \(a_q\) 取出来,则 \((p,q)\) 内的点都集中匹配于 \(y\) 的一个点上即可,这个点是 \(p\) 覆盖部分与 \(q\) 覆盖部分之间的最小值,如果这也不行,则不合法。但是我们还不知道 \(p\) 和 \(q\) 覆盖了什么,所以需要进一步考虑。观察不断向左处理的过程,发现我们相当于取出了 \(x\) 的所有前缀 \(\max\)。从左到右处理,维护当前 \(premax\) 前面的数至少(因为后面总有更大的数兜底,所以尽量少匹配不劣)要覆盖到哪里,记为 \(j\)。设当前 \(premax\) 与下一个 \(premax\) 之间最小的 \(x_i\) 为 \(mn\),则找到 \(j'\ge j\) 中第一个满足 \(y_{j'}<mn\) 的位置,将 \(j\sim j'\) 用当前 \(premax\) 覆盖(若无法覆盖,就不合法,因为前面也不存在能覆盖这个数的点),然后 \(j\leftarrow j'\)。
右侧对称地处理。若最终 \(j_{left}\le j_{right}\),则合法,否则不合法。
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
typedef pair<int,int> pii;
typedef pair<int,ll> pil;
typedef pair<ll,int> pli;
typedef pair<ll,ll> pll;
template<typename T>
void chkmin(T &x,const T &y){x=min(x,y);}
template<typename T>
void chkmax(T &x,const T &y){x=max(x,y);}
const int inf=0x3f3f3f3f;
const ll infll=0x3f3f3f3f3f3f3f3f;
const int N=500005;
int n0,m0,q,a[N],b[N];
int n,m,x[N],y[N];
void calc(){
if(x[1]<y[1]) swap(n,m),swap(x,y);
// printf("\n");
// for(int i=1;i<=n;i++) printf("%d ",x[i]);printf("\n");
// for(int i=1;i<=n;i++) printf("%d ",y[i]);printf("\n");
int mx=-1,mid=0;
for(int i=1;i<=n;i++) if(x[i]>mx) mx=x[i],mid=i;
for(int i=1;i<=m;i++) if(y[i]>=mx){
printf("0");
return;
}
mx=0;
int mn=inf,j=1;
for(int i=1;i<=mid;i++){
if(x[i]>=mx){
while(j<=m&&y[j]>=mn){
if(y[j]>=mx){
printf("0");
return;
}
j++;
}
if(j>m||x[i]<=y[j]){
printf("0");
return;
}
mn=inf,mx=x[i];
}
else chkmin(mn,x[i]);
}
mx=0,mn=inf;
int k=m;
for(int i=n;i>=mid;i--){
if(x[i]>=mx){
while(k>=1&&y[k]>=mn){
if(y[k]>=mx){
printf("0");
return;
}
k--;
}
if(k<1||x[i]<=y[k]){
printf("0");
return;
}
mn=inf,mx=x[i];
}
else chkmin(mn,x[i]);
}
// printf("%d %d\n",j,k);
if(j<=k) printf("1");
else printf("0");
}
void __INIT__(){}
void __SOLVE__(){
scanf("%d%d%d",&n0,&m0,&q);
for(int i=1;i<=n0;i++) scanf("%d",&a[i]);
for(int i=1;i<=m0;i++) scanf("%d",&b[i]);
n=n0,m=m0;
memcpy(x,a,sizeof(a)),memcpy(y,b,sizeof(b));
calc();
while(q--){
n=n0,m=m0;
memcpy(x,a,sizeof(a)),memcpy(y,b,sizeof(b));
int k1,k2;
scanf("%d%d",&k1,&k2);
for(int i=1;i<=k1;i++){
int p,v;
scanf("%d%d",&p,&v);
x[p]=v;
}
for(int i=1;i<=k2;i++){
int p,v;
scanf("%d%d",&p,&v);
y[p]=v;
}
calc();
}
printf("\n");
}
int main(){
int T=1;
scanf("%*d");
__INIT__();
while(T--) __SOLVE__();
}
ARC101F/D Robots and Exits
考虑两个机器人之间相互的限制:
如果有至少一个机器人不在两个出口之间(即在两端),忽略。
否则,设两个机器人到两个出口距离分别为 \(a,b,c,d\),如图

有四种情况:
LL(两个机器人均从左侧与其相临的出口离开,下同理),这种情况总能做到。RR总能做到。LR若第一个机器人先走,\(a<c\),第二个机器人先走,\(b>d\)。两者取或,即 \(a<c\) 或 \(b>d\)。RL本来笔者进行了推导,但后面发现,这种情况与LR本质相同(交换了两个机器人的名称),不过多赘述。
于是变成一个 2-SAT 问题:对于 LR 不合法,即 \(a\ge c\) 且 \(b\le d\) 的情况,从第一个机器人向第二个机器人连边(其实是 \(u_0\rightarrow v_0,v_1\rightarrow u_1\),两边对称,只需要考虑一边)。
放在二维平面上。一个点会向其左上方的点连边,选了一个点(让这个点从左边出去),会让其左上方的点都选中。考虑把原题中合法方案做一一对应,发现可以对应到一个上升子序列(这代表了二维平面上选中的点的右下边缘)。
考虑 DP 求严格上升子序列数量,离散化+树桩数组做到 \(\mathcal{O}(n\log n)\),注意去重即可。
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
typedef pair<int,int> pii;
template<typename T>
void chkmin(T &x,const T &y){x=min(x,y);}
template<typename T>
void chkmax(T &x,const T &y){x=max(x,y);}
const int inf=0x3f3f3f3f;
const ll infll=0x3f3f3f3f3f3f3f3f;
const int MOD=1000000007;
const int N=100005;
int n,m,x[N],y[N];
pii p[N];
unordered_map<int,int> mp;
int tr[N];
#define lowbit(x) (x&(-x))
void add(int x,int a){
for(;x<=n;x+=lowbit(x)) tr[x]=(tr[x]+a)%MOD;
}
int query(int x){
int sum=1;// f[0]=1
for(;x;x-=lowbit(x)) sum=(sum+tr[x])%MOD;
return sum;
}
int main(){
scanf("%d%d",&n,&m);
for(int i=1;i<=n;i++) scanf("%d",&x[i]);
for(int i=1;i<=m;i++) scanf("%d",&y[i]);
vector<int> nums;
int cnt=0;
for(int i=1,j=1;i<=n;i++){
if(x[i]<y[1]||x[i]>y[m]) continue;
cnt++;
while(y[j+1]<x[i]) j++;
p[cnt]={x[i]-y[j],y[j+1]-x[i]};
nums.push_back(y[j+1]-x[i]);
}
if(!cnt){
printf("1\n");
return 0;
}
sort(nums.begin(),nums.end());
nums.erase(unique(nums.begin(),nums.end()),nums.end());
for(int i=0;i<nums.size();i++) mp[nums[i]]=i+1;
for(int i=1;i<=cnt;i++) p[i].second=mp[p[i].second];
sort(p+1,p+cnt+1,[&](pii x,pii y){
if(x.first!=y.first) return x.first<y.first;
return x.second>y.second;
});
cnt=unique(p+1,p+cnt+1)-p-1;
for(int i=1;i<=cnt;i++) add(p[i].second,query(p[i].second-1));
printf("%d\n",query(cnt));
return 0;
}
P7011 [CERC2013] Escape
同类题:HDU6326 Monster Hunter (QOJ7588),QOJ7520 Monster Generator。
考虑树上 DP,但直接做复杂度爆了,考虑把树转为一系列没有先后顺序要求的二元组 \((x,y),x\ge 0,y>0\),表示当 \(HP\ge x\) 时可以获得 \(y\) 的增量。求答案时按 \(x\) 从小到大遍历所有二元组模拟即可。
先把所有子树合并,接下来要处理 \(u\) 的限制。
若 \(a_u>0\),加入 \((0,a_u)\) 不难证明仍符合题意。若 \(a_u=0\),什么都不做,仍符合题意。
若 \(a_u<0\),初始二元组为 \((|a_u|,a_u)\),但如果直接加入会有两个问题:1) 有一些二元组的 \(x<|a_u|\),2) \(a_u<0\) 不满足前述 \(y>0\) 的条件。
考虑按 \(x\) 从小到大考虑子树中已有二元组,设当前 \(u\) 对应二元组为 \((X,Y)\),若 \(x<X\) 或 \(Y<0\),将 \((x,y)\) 与 \((X,Y)\) 合并,\((X,Y)\leftarrow (\max(X,x-Y),y+Y)\)。做完之后,若 \(Y\le 0\),将 \((X,Y)\) 扔去,否则加入堆中。
关于合并二元组:
若 \(Y<0\),则不取 \((x,y)\) 一定不优,取了的话就必然要求 \(HP\ge X\) 且 \(HP+Y\ge x\),所以有上面的转移。
若 \(x<X\),则取了 \((X,Y)\) 后 \(HP\) 不会 \(<x\),所以进行上述合并不劣。
用堆维护二元组,合并子树进行启发式合并即可。
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
typedef pair<int,int> pii;
typedef pair<int,ll> pil;
typedef pair<ll,int> pli;
typedef pair<ll,ll> pll;
typedef priority_queue<pll,vector<pll>,greater<pll>> pqu;
template<typename T>
void chkmin(T &x,const T &y){x=min(x,y);}
template<typename T>
void chkmax(T &x,const T &y){x=max(x,y);}
const int inf=0x3f3f3f3f;
const ll infll=0x3f3f3f3f3f3f3f3f;
const int N=200005;
int n,t;
ll a[N];
vector<int> G[N];
void merge(pqu &x,pqu &y){
if(x.size()<y.size()) swap(x,y);
for(;y.size();y.pop()) x.push(y.top());
}
void dfs(int u,int f,pqu &pq){
for(auto v:G[u]){
if(v==f) continue;
pqu tmp;
dfs(v,u,tmp);
merge(pq,tmp);
}
if(a[u]>0) pq.push({0,a[u]});
else if(a[u]<0){
pll p{-a[u],a[u]};
while(pq.size()&&(pq.top().first<p.first||p.second<0)){
pll q=pq.top();
pq.pop();
p={max(p.first,q.first-p.second),p.second+q.second};
}
if(p.second>0) pq.push(p);
}
// printf("u=%d\n",u);
// pqu tmp=pq;
// for(;tmp.size();tmp.pop()) printf("(%lld,%lld) ",tmp.top());
// printf("\n");
}
void __INIT__(){}
void __CLEAR__(){
for(int i=1;i<=n+1;i++) G[i].clear();
}
void __SOLVE__(){
scanf("%d%d",&n,&t);
__CLEAR__();
a[n+1]=infll;
for(int i=1;i<=n;i++) scanf("%lld",&a[i]);
for(int i=1;i<n;i++){
int u,v;
scanf("%d%d",&u,&v);
G[u].push_back(v),G[v].push_back(u);
}
G[t].push_back(n+1),G[n+1].push_back(t);
pqu pq;
dfs(1,1,pq);
ll hp=0;
for(;pq.size()&&pq.top().first<=hp;pq.pop()) hp+=pq.top().second;
ll sum=0;
for(int i=1;i<=n;i++) sum+=max(a[i],0ll);
if(hp>sum) printf("escaped\n");
else printf("trapped\n");
}
int main(){
int T=1;
scanf("%d",&T);
__INIT__();
while(T--) __SOLVE__();
return 0;
}
P11714 [清华集训 2014] 主旋律
(补,以前做的)
不知道洛谷题解为什么都要绕一圈……设 \(f_S\) 表示点集 \(S\) 答案,枚举 \(i\) 表示缩点后零度点数量,二项式反演,钦定 \(j\) 个零度点的方案数是 \(\sum\limits_{T\subseteq S}g_{T,j}2^{E_{T,S-T}+E_{S-T,S-T}}\),其中 \(g_{S,i}\) 表示将 \(S\) 中的点组成 \(i\) 个独立的强连通块方案数。\(f\) 转移为:
其中 \(g_S=\sum\limits_{i=1}^n(-1)^{i+1}g_{S,i}\),转移为
注意 \(+f_S\) 一项应在算完 \(S\) 后计算(可能是废话)。
考虑求 \(E\),有两种:
- \(E_{S,S}\leftarrow E_{S-u,S-u}+|in_u\And S|+|out_u\And S|\),其中 \(u=\min S\)
- \(E_{S,T}\leftarrow E_{S,T-u}+|in_u\And S|\),其中 \(u=\min T\)(这句话是口胡的,不知道对不对,好像和之前代码里写的不一样)
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
typedef pair<int,int> pii;
typedef pair<int,ll> pil;
typedef pair<ll,int> pli;
typedef pair<ll,ll> pll;
template<typename T>
void chkmin(T &x,const T &y){x=min(x,y);}
template<typename T>
void chkmax(T &x,const T &y){x=max(x,y);}
const int MOD=1000000007;
void add(int &x,int y){
x+=y;
if(x>=MOD) x-=MOD;
}
const int N=20,PW=(1<<15),PW3=14348907;
int n,m,f[PW],e1[PW],e2[PW3],p[PW],pw2[N*N],pw3[N];
vector<int> G[N],H[N];
#define lowbit(x) ((x)&(-(x)))
#define lb(x) (31^__builtin_clz(lowbit(x)))
int id(int s,int t){
int x=0;
for(int i=1;i<=n;i++){
if((s>>(i-1))&1) x+=pw3[i-1];
if((t>>(i-1))&1) x+=2*pw3[i-1];
}
return x;
}
int main(){
scanf("%d%d",&n,&m);
pw2[0]=pw3[0]=1;
for(int i=1;i<=n*n;i++) pw2[i]=2*pw2[i-1]%MOD;
for(int i=1;i<=n;i++) pw3[i]=3*pw3[i-1];
for(int i=1;i<=m;i++){
int u,v;
scanf("%d%d",&u,&v);
G[u].push_back(v);
H[u].push_back(v),H[v].push_back(u);
}
e1[0]=0;
for(int s=1;s<(1<<n);s++){
int i=lb(s)+1,cnt=0;
for(auto j:H[i]) if((s>>(j-1))&1) cnt++;
e1[s]=e1[s^(1<<(i-1))]+cnt;
}
for(int t=0;t<(1<<n);t++){
vector<int> v;
v.push_back(0);
for(int i=1;i<=n;i++){
if((t>>(i-1))&1) continue;
int cnt=0;
for(auto j:G[i]) if((t>>(j-1))&1) cnt++;
vector<int> tmp=v;
for(auto s:v){
e2[id(s|(1<<(i-1)),t)]=e2[id(s,t)]+cnt;
tmp.push_back(s|(1<<(i-1)));
}
v.swap(tmp);
}
}
for(int s=0;s<(1<<n);s++){
for(int t=s;t;t=(t-1)&s){
if(t==s) continue;
add(f[s],(ll)p[t]*pw2[e2[id(t,s^t)]+e1[s^t]]%MOD);
if(lb(s)==lb(t)){
add(f[s],MOD-(ll)p[s^t]*(pw2[e1[t]]-f[t]+MOD)%MOD);
add(p[s],MOD-(ll)p[s^t]*(pw2[e1[t]]-f[t]+MOD)%MOD);
}
}
add(p[s],(pw2[e1[s]]-f[s]+MOD)%MOD);
}
printf("%d\n",(MOD+pw2[m]-f[(1<<n)-1])%MOD);
return 0;
}
P9189 [USACO23OPEN] Custodial Cleanup G
日常被图论蓝题击败。
考虑一个不劣的方案是先把能拿的钥匙都拿了,再放置。
拿钥匙可以魔改 bfs。维护已有颜色的 \(vis\),到一个点,如果还无法开锁,就存进一个临时数组,否则入队,获得一个钥匙之后,把临时数组里同色的结点入队。
注意到取/放钥匙的过程可逆,如果能在 \(S\) 状态下取出钥匙,就能按原样放回去。
笔者尝试考虑交换相邻两个结点钥匙,但是假了。
(后面不会)
考虑把放钥匙的过程求逆。保留第一次 bfs 能到的点,令 \(S_i\leftarrow F_i\),再跑一遍,不一样的地方在于如果 \(C_i=S_i\),那么 \(i\) 可以直接进入,不需要等钥匙。
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
typedef pair<int,int> pii;
typedef pair<int,ll> pil;
typedef pair<ll,int> pli;
typedef pair<ll,ll> pll;
template<typename T>
void chkmin(T &x,const T &y){x=min(x,y);}
template<typename T>
void chkmax(T &x,const T &y){x=max(x,y);}
const int inf=0x3f3f3f3f;
const ll infll=0x3f3f3f3f3f3f3f3f;
const int MOD=998244353;
void add(int &x,int y){
x+=y;
if(x>=MOD) x-=MOD;
}
int qpow(int a,ll b){
int mul=1;
while(b){
if(b&1) mul=(ll)mul*a%MOD;
a=(ll)a*a%MOD;
b>>=1;
}
return mul;
}
const int N=100005;
int n,m,c[N],s[N],f[N],vis[N],visc[N];
vector<int> G[N],nds[N];
void bfs(bool flag){
for(int i=1;i<=n;i++) nds[i].clear(),visc[i]=0;
queue<int> q;
q.push(1);
while(!q.empty()){
int u=q.front();
q.pop();
if(vis[u]) continue;
vis[u]=1;
for(auto v:G[u]){
if(visc[c[v]]||(flag&&c[v]==s[v])) q.push(v);
else nds[c[v]].push_back(v);
}
if(!visc[s[u]]){
visc[s[u]]=1;
for(auto v:nds[s[u]]) q.push(v);
}
}
}
void __INIT__(){}
void __CLEAR__(){
for(int i=1;i<=n;i++) G[i].clear();
}
void __SOLVE__(){
scanf("%d%d",&n,&m);
__CLEAR__();
for(int i=1;i<=n;i++) scanf("%d",&c[i]);
for(int i=1;i<=n;i++) scanf("%d",&s[i]);
for(int i=1;i<=n;i++) scanf("%d",&f[i]);
for(int i=1;i<=m;i++){
int u,v;
scanf("%d%d",&u,&v);
G[u].push_back(v),G[v].push_back(u);
}
for(int i=1;i<=n;i++) vis[i]=0;
bfs(0);
for(int i=1;i<=n;i++){
if(!vis[i]&&s[i]!=f[i]){
printf("NO\n");
return;
}
swap(s[i],f[i]);
vis[i]^=1;
}
bfs(1);
for(int i=1;i<=n;i++) if(!vis[i]&&s[i]!=f[i]){
printf("NO\n");
return;
}
printf("YES\n");
}
int main(){
int T=1;
scanf("%d",&T);
__INIT__();
while(T--) __SOLVE__();
return 0;
}

浙公网安备 33010602011771号