orangejuice's blog

挂了请务必叫我

CF Round#698 Div1. Nezzar and Chocolate Bars

前言

这就是大道至简吗。。

为什么和某ZJOI开关一样,到最后就是个背包。。


题目大意:

给定 \(n,K\) 和一些棍子长度为 \(l_i(i\in [1,n])\) (实数!)

每次随机选择一根棍子,概率与 \(l_i\) 成正比,然后随机分裂成两段(实数!)

一直分裂,直到每根棍子 \(l_i\leq K\) ,求期望分裂次数

\[ \ \]

根据题意容易归纳一个更直观的模型:

把这些 \(l_i\) 排在数轴上,初始时,点集为 \(0,l_1,l_1+l_2,\ldots\)

每次随机在数轴上撒一个点,直到点集中相邻两点距离 \(\leq K\) 为止

考虑从简单情况入手

\[ \ \]

n=1

单棍情况,设长度为 \(L\) ,考虑期望的简单等价变换

\(\displaystyle E(\text{条件第一次成立操作数})=\sum_{i=0}^{\infty} P(i次操作条件未成立)\)

\(P_n\) 为操作 \(n\) 次成立(不是恰好)的概率 (变量名重了不要介意)

\(n\) 次操作后的点集排列之后为 \(X_i\) ,其中 \(X_0=0,X_{n+1}=L\)

我们要求 \(\forall i\in[1,n+1],Z_i=X_i-X_{i-1}\leq K\) ,考虑用一个二项式反演来解决这个计算

\(W=\lfloor \frac{L}{K}\rfloor\)

\(\displaystyle P_n=\frac{1}{L^n}\sum_{i=0}^{W} (-1)^i\binom{n+1}{i}(L-iK)^n\)

其意义即为从分成的 \(n+1\)\(Z_i\) 中选择 \(i\) 个强制不合法,然后剩下的随意分布,由此计算概率

\[ \ \]

一般情形

考虑一样的期望转概率,设 \(p_m\)\(m\) 次完成的概率, \(q_m=1-p_m\)

\(\displaystyle q_m=\sum_{j_1+j_2+\cdots +j_n=m} \frac{m!}{j_1!j_2!\cdots j_n!}\prod (\frac{l_i}{\sum l_k})^{j_i}P_{i,j_i}\)

显然这里考虑用 \(\text{EGF}\) 积的形式来表示,设 \(S=\sum l_i\)

回到上一步,这里我们对于 \(i,l_i\) 计算其 \(P_n\) 加上 \(\frac{l_i}{S}\) 系数的 \(\text{EGF}\) ,沿用上面的 \(L=l_i\)

\(\displaystyle F_i(x)=\text{EGF(P)}=\sum_{n\ge 0}\frac{1}{n!}(\frac{L}{S})^n\sum_{i=0}^W(-1)^{i}\binom{n+1}{i}(1-i\frac{K}{L})^{n}x^n\)

\(\displaystyle F_i(x)=\sum_{n\ge 0}\frac{1}{n!}\sum_{i=0}^W(-1)^{i}\binom{n+1}{i}(\frac{L-iK}{S})^{n}x^n\)

\(\displaystyle u=\frac{L-iK}{S}x\)

\(\displaystyle F_i(x)=\sum_{i=0}^W\frac{(-1)^{i}}{i!}\sum_{n\ge i-1}\frac{n+1}{(n+1-i)!}u^n\)

\(n'=n+1-i\)\(n=n'+i-1\) ,将 \(n'\) 带入

\(\displaystyle F_i(x)=\sum_{i=0}^W\frac{(-1)^{i}}{i!}\sum_{n\ge 0}\frac{n+i}{n!}u^{n+i-1}\)

\(n,i\) 拆开

\(\displaystyle F_i(x)=\sum_{i=0}^W \frac{(-1)^{i}}{i!}(u^{i}\sum_{n\ge 0}\frac{1}{n!}u^{n}+u^{i-1}\sum_{n\ge 0}\frac{i}{n!}u^{n})\)

\(\displaystyle F_i(x)=\sum_{i=0}^W \frac{(-1)^{i}}{i!}(u^{i}+iu^{i-1})e^u\)

容易发现我们需要多项式是 \(F(x)=e^x-\prod F_i(x)\)

最终 \(F(x)\) 的每一项,都会是 \(x^k e^{cx}\) 的形式

其中 \(e^u\) 中的 \(u\) 总是 \(\frac{1}{S}(L-iK)x\) ,合并之后 \(L\) 之和总是存在,记录 \(\sum i\) 即可,为 \(O(S)\)

而每项要么是 \(u^ie^u\) 要么是 \(u^{i-1}e^u\) ,可以考虑记录一下 \(u^{i-1}e^u\) 出现的次数,为 \(O(n)\)

我们的多项式项数是 \(O(nS)\)

\[ \ \]

最终我们还需要对于每一项计算答案

我们要计算 \(\displaystyle \sum_{n\ge 0}n![x^n]F(x)\) ,考虑形如 \(x^ke^{cx}\) 一项的贡献

\(\displaystyle \sum_{n\ge k}n![x^n](x^k e^{cx})=\sum_{n\ge k}\frac{n!}{(n-k)!}c^{n-k}=k!\sum_{n\ge 0}\binom{n+k}{n}c^{n}\)

由于 \(\displaystyle \binom{n+k}{n}=(-1)^{n}\binom{-k-1}{n}\)

带入广义二项式定理得到

\(\displaystyle k!\sum_{n\ge 0}\binom{n+k}{n}c^{n}=k!(1-c)^{-k-1}\)

根据是否用 \(\text{NTT}\) 优化,复杂度会有所不同

over.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
#include<bits/stdc++.h>
using namespace std;

typedef double db;
typedef long double ldb;
typedef long long ll;
typedef unsigned long long ull;
#define reg register
#define pb push_back
#define mp make_pair
#define Mod1(x) ((x>=P)&&(x-=P))
#define Mod2(x) ((x<0)&&(x+=P))
#define rep(i,a,b) for(int i=a,i##end=b;i<=i##end;++i)
#define drep(i,a,b) for(int i=a,i##end=b;i>=i##end;--i)
template <class T> inline void cmin(T &a,const T &b){ ((a>b)&&(a=b)); }
template <class T> inline void cmax(T &a,const T &b){ ((a<b)&&(a=b)); }

char IO;
template <class T=int> T rd(){
T s=0; int f=0;
while(!isdigit(IO=getchar())) if(IO=='-') f=1;
do s=(s<<1)+(s<<3)+(IO^'0');
while(isdigit(IO=getchar()));
return f?-s:s;
}

const int N=1<<11|10,P=998244353;

int n,m,k;
ll qpow(ll x,ll k=P-2) {
ll res=1;
for(;k;k>>=1,x=x*x%P) if(k&1) res=res*x%P;
return res;
}

int rev[N],I[N],J[N];
typedef vector <int> V;
void Init(){
rep(i,J[0]=1,N-1) J[i]=1ll*J[i-1]*i%P;
I[N-1]=qpow(J[N-1]);
drep(i,N-1,1) I[i-1]=1ll*I[i]*i%P;
}
int Init(int n){
int R=1,c=-1;
while(R<=n) R<<=1,c++;
rep(i,0,R-1) rev[i]=(rev[i>>1]>>1)|((i&1)<<c);
return R;
}
void NTT(int n,V &a,int f) {
static int e[N>>1];
rep(i,0,n-1) if(i<rev[i]) swap(a[i],a[rev[i]]);
for(int i=e[0]=1;i<n;i<<=1) {
ll t=qpow(f==1?3:(P+1)/3,(P-1)/i/2);
for(int j=i-2;j>=0;j-=2) e[j+1]=(e[j]=e[j>>1])*t%P;
for(int l=0;l<n;l+=i*2) {
for(int j=l;j<l+i;++j) {
int t=1ll*e[j-l]*a[j+i]%P;
a[j+i]=a[j]-t,Mod2(a[j+i]);
a[j]+=t,Mod1(a[j]);
}
}
}
if(f==-1) {
ll Inv=1ll*I[n]*J[n-1]%P;
rep(i,0,n-1) a[i]=a[i]*Inv%P;
}
}

V operator * (V a,V b){
if(!a.size() || !b.size()) return {};
int n=a.size()+b.size()-1,R=Init(n);
a.resize(R),b.resize(R);
NTT(R,a,1),NTT(R,b,1);
rep(i,0,R-1) a[i]=1ll*a[i]*b[i]%P;
NTT(R,a,-1);
a.resize(n);
return a;
}
V operator + (V a,V b){
if(a.size()<b.size()) swap(a,b);
rep(i,0,b.size()-1) a[i]+=b[i],Mod1(a[i]);
return a;
}

V F[55],A,B;
int S,L[55];

int main(){
Init();
n=rd(),k=rd();
rep(i,1,n) S+=L[i]=rd();
sort(L+1,L+n+1);
int InvS=qpow(S);
F[0]={1};
rep(i,1,n) {
int W=(L[i]-1)/k;
A.clear(),A.resize(W+1);
B.clear(),B.resize(W+1);
// 注意 L[i]==j*k && j==1 时会出现side condition
// 也就是 L=k=1 的情况
rep(j,0,W) {
int w=1ll*(L[i]-j*k)*InvS%P;
A[j]=1ll*(j&1?P-I[j]:I[j])*qpow(w,j)%P;
if(j) B[j]=1ll*(j&1?P-I[j]:I[j])*qpow(w,j-1)%P*j%P;
}
drep(j,i,0) {
if(!j) F[j]=F[j]*A;
else F[j]=F[j]*A+F[j-1]*B;
}
}
// 就硬乘。。
// 如果分治合并每个,复杂度变为log n
int ans=0;
rep(i,0,n) {
rep(j,max(i,1),F[i].size()-1) if(F[i][j]) {
int k=j-i;
ans=(ans+1ll*F[i][j]*J[k]%P*qpow(1ll*j*::k*InvS%P,P-1-k-1))%P;
}
}
ans=(P-ans)%P;
printf("%d\n",ans);
}

CF1037H - Security

题目大意

给定一个串 \(S\) ,每次查询一个区间 \([l,r]\) 和一个串 \(T\)

\([l,r]\) 内字典序 \(>T\) 的最小的子串 \(R\)


分析

~~复习 \(\text{SAM}\) ~~

显然可以枚举 \(T\) 匹配 \(R\) 的长度,然后枚举下一位字符,判断形成的串是否在 \([l,r]\) 内有出现

匹配问题用 \(\text{SAM}\) 维护比较方便,求出匹配节点后,判断其 \(\text{endpos}\) 集是否包含所求范围

可以在线线段树合并,也可以将所所有询问离线后树状数组+ \(\text{dfs}\) 作差

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
const int N=2e5+10,INF=1e9+10;

int n,m;
char s[N];
int trans[N][26],link[N],len[N],End[N],cnt,lst;
void Extend(int c){
int p=lst,cur=++cnt; len[cur]=len[lst]+1,lst=cur;
End[cur]=len[cur];
while(~p && !trans[p][c]) trans[p][c]=cur,p=link[p];
if(p==-1){ link[cur]=0; return; }
int q=trans[p][c];
if(len[q]==len[p]+1){ link[cur]=q; return; }
int r=++cnt;
len[r]=len[p]+1,memcpy(trans[r],trans[q],104);
while(~p && trans[p][c]==q) trans[p][c]=r,p=link[p];
link[r]=link[q],link[q]=link[cur]=r;
}

const int M=N*32;
int ls[M],rs[M];
void Upd(int &p,int l,int r,int x){
p=++cnt;
if(l==r) return;
int mid=(l+r)>>1;
x<=mid?Upd(ls[p],l,mid,x):Upd(rs[p],mid+1,r,x);
}
int Uni(int x,int y) {
if(!x||!y) return x|y;
int z=++cnt;
ls[z]=Uni(ls[x],ls[y]),rs[z]=Uni(rs[x],rs[y]);
return z;
}
int Que(int p,int l,int r,int ql,int qr) {
if(ql>qr || !p) return 0;
if(ql<=l && r<=qr) return 1;
int mid=(l+r)>>1;
if(ql<=mid && Que(ls[p],l,mid,ql,qr)) return 1;
if(qr>mid && Que(rs[p],mid+1,r,ql,qr)) return 1;
return 0;
}
vector <int> G[N];

int rt[N];
void dfs(int u) {
if(End[u]) Upd(rt[u],1,n,End[u]);
for(int v:G[u]) dfs(v),rt[u]=Uni(rt[u],rt[v]);
}

int K[N];
void Solve(){
int l=rd(),r=rd();
scanf("%s",s+1),m=strlen(s+1),s[m+1]='a'-1;
int p=0;
rep(i,1,m) {
K[i]=trans[K[i-1]][s[i]-'a'];
if(!K[i]) break;
p=i;
}
drep(i,p,0) {
rep(j,s[i+1]-'a'+1,25) {
int x=trans[K[i]][j];
if(!x) continue;
if(!Que(rt[x],1,n,l+i,r)) continue;
s[i+1]=j+'a',s[i+2]=0,puts(s+1);
return;
}
}
puts("-1");
}

int main(){
link[0]=-1,scanf("%s",s+1),n=strlen(s+1);
rep(i,1,n) Extend(s[i]-'a');
rep(i,1,cnt) G[link[i]].pb(i);
cnt=0,dfs(0);
rep(_,1,rd()) Solve();
}

CF1061E - Politics

题目大意

给定两棵有根树 \(T_1,T_2\) ,节点均从 \(1-n\) 编号

对于节点 \(i\) ,有权值 \(a_i\) ,每个节点可以被选择一次

对于 \(T_1,T_2\) ,有 \(q_1,q_2\) 条限制,每条限制了一个子树 \(k\) 内恰好有 \(x\) 个点被选择

求最大化选择的权值之和,或者确定不存在方案


分析

如果剥离树的结构考虑,让问题变成点集之和的限制

这是一个经典的无法处理的问题,或许可以单纯形试试看

那么考虑树的结构如何处理

我们认为一个点被选择会向祖先链上的点总和+1,这是一个链状更新

可以用一棵内向树描述,再将限制加在边上,就能够得到一个简单的网络流模型

然而边权无法限制满流(除非上下界网络流),而现在问题不仅带权,还同时涉及两棵树

因此引入费用是必须的

解决恰好为 \(x\) 的限制

考虑将有限制的边视作特殊,我们将这条边额外加上一个费用 \(\infty\) ,最后从答案中减去

如果最优解中无法流满这些边,答案将 \(<0\)


解决两棵树

Naive的思路,我们需要强制两棵树上同编号的点入流相同

经过长时间弱智的思考,这无法实现

于是想办法强制两个点入流不同

容易发现,对于被选个数的限制可以对称转化为限制未选个数

从源点 \(S\)\(i_0\) 连一条流量为1的边, \(i_0\) 流向 \(i_1\) 表示选择 \(i\) ,流向 \(i_2\) 表示不选

\(T_1\) 建成选择点的限制, \(T_2\) 建成不选点的限制即可


形式化的说,对于一个费用流,我们想要限制两条边 \(flow_1=flow_2\)

\(flow_1-flow_2=0\) ,这难以做到

但是我们可以限制 \(flow_1+(-flow_2)=0\) ,或者说

\(flow_1+(w-flow_2)=w\)

并且将对于 \(flow_2\) 的限制转化为对 \(w-flow_2\) 的限制,此时额外建立一个点 \(t\)

\(S\)\(t\)\((w,\infty)\) ,强制向 \(t\) 流满 \(w\) 的流量,然后再从 \(t\)\(flow_1,w-flow_2\) 分流即可


\(\text{EK}\) 费用流 QQ图片20210506190147.jpg

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
#include<bits/stdc++.h>
using namespace std;

typedef long long ll;
typedef unsigned long long ull;
typedef double db;
typedef pair <int,int> Pii;
#define reg register
#define mp make_pair
#define pb push_back
#define Mod1(x) ((x>=P)&&(x-=P))
#define Mod2(x) ((x<0)&&(x+=P))
#define rep(i,a,b) for(int i=a,i##end=b;i<=i##end;++i)
#define drep(i,a,b) for(int i=a,i##end=b;i>=i##end;--i)
template <class T> inline void cmin(T &a,T b){ ((a>b)&&(a=b)); }
template <class T> inline void cmax(T &a,T b){ ((a<b)&&(a=b)); }

char IO;
template <class T=int> T rd(){
T s=0; int f=0;
while(!isdigit(IO=getchar())) f|=IO=='-';
do s=(s<<1)+(s<<3)+(IO^'0');
while(isdigit(IO=getchar()));
return f?-s:s;
}

const int N=1544,INF=1e9+7;

int n,m,d;
int a[N];
struct Edge{
int to,nxt,w,c;
} e[N*4];
int head[N],ecnt=1;
void AddEdge(int u,int v,int w,int c){
e[++ecnt]=(Edge){v,head[u],w,c};
head[u]=ecnt;
}
void Link(int u,int v,int w,int c){ AddEdge(u,v,w,c),AddEdge(v,u,0,-c); }
int S=1,T=2,V=2;

ll ans=0;
struct Tree{
vector <int> G[N];
int Rt,sz[N],lim[N],fa[N];
void dfs(int u,int f) {
//cout<<"pre dfs "<<u<<endl;
sz[u]=1,lim[u]=-1,fa[u]=f;
for(int v:G[u]) if(v!=f) {
dfs(v,u);
sz[u]+=sz[v];
}
}
void ReadTree(){
rep(i,2,n){
int u=rd(),v=rd();
G[u].pb(v),G[v].pb(u);
}
dfs(Rt,0);
}
void ReadLimit(){
rep(_,1,rd()) {
int u=rd(),x=rd();
//cout<<" $"<<sz[u]<<endl;
if(sz[u]<x) puts("-1"),exit(0);
lim[u]=x;
}
}
void Init(int k){
fa[Rt]=T-V;
if(k==0) {
rep(i,1,n) {
Link(T+i,V+i,1,a[i]);
if(~lim[i]) {
ans-=1ll*lim[i]*INF;
Link(V+i,V+fa[i],lim[i],INF);
} else Link(V+i,V+fa[i],n,0);
}
} else {
rep(i,1,n) {
Link(T+i,V+i,1,0);
if(~lim[i]) {
ans-=1ll*(sz[i]-lim[i])*INF;
Link(V+i,V+fa[i],sz[i]-lim[i],INF);
} else Link(V+i,V+fa[i],n,0);
}
}
V+=n;
}
} Tr[2];

ll dis[N];
int vis[N],pre[N];
int SPFA(){
static queue <int> que;
rep(i,1,V) dis[i]=-1e18;
que.push(S),dis[S]=0;
while(!que.empty()) {
int u=que.front(); que.pop();
vis[u]=0;
for(int i=head[u];i;i=e[i].nxt){
int v=e[i].to,c=e[i].c;
if(dis[v]>=dis[u]+c || !e[i].w) continue;
dis[v]=dis[u]+e[i].c,pre[v]=i;
//cout<<"$ "<<u<<' '<<v<<' '<<e[i].c<<' '<<e[i].w<<endl;
if(!vis[v]) vis[v]=1,que.push(v);
}
}
return dis[T]>-1e18;
}

void EK(){
while(SPFA()) {
int w=INF;
for(int u=T;pre[u];u=e[pre[u]^1].to) cmin(w,e[pre[u]].w);
for(int u=T;pre[u];u=e[pre[u]^1].to) e[pre[u]].w-=w,e[pre[u]^1].w+=w;
ans+=w*dis[T];
}
}

int main(){
n=rd(),Tr[0].Rt=rd(),Tr[1].Rt=rd();
rep(i,1,n) Link(S,V+i,1,INF),ans-=INF;
V+=n;
rep(i,1,n) a[i]=rd();
rep(i,0,1) Tr[i].ReadTree();
rep(i,0,1) Tr[i].ReadLimit(),Tr[i].Init(i);
EK();
if(ans<0) puts("-1");
else printf("%lld\n",ans);
}

CF1082F - Speed Dial

题目大意

给定 \(n\) 个电话号码,你可以随意生成 \(k\) 个快捷键,每个快捷键是一个数字串

最终拨号方式:

选择 至多一个 快捷键按下,对于剩余部分手动补全,且不允许退格

每个电话号码有拨打次数,最小化手动补全部分的长度总和


分析

如果每次选定一个集合使其公用一个快捷键,那么其长度必然是集合中所有串的 \(\text{LCP}\)

假设确定了一个 \(\text{LCP}\) ,那么对应的串集合容易发现就是 \(\text{trie}\) 树上的一个子树

于是先将所有串加入 \(\text{trie}\) 树,此时问题转化为

选择至多 \(k\)\(\text{LCP}\) (默认根节点选了且没有代价),使得每个电话号码到其祖先中最深 \(\text{LCP}\) 的距离之和最小

由于有拨打次数的限制,且其数值相对较大,难以存入dp状态

于是想到在祖先钦定 \(\text{LCP}\) ,然后从子树取值

\(dp_{u,i,j}\) 表示计算 \(u\) 子树内的答案,已经钦定祖先中最深的 \(\text{LCP}\) 长度为 \(i\) ,且在子树内又钦定了 \(j\)\(\text{LCP}\)

合并子树时对于每个 \(i\) ,背包合并 \(j\) ,决策 \(u\) 自己是否选为 \(\text{LCP}\) 即可

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45

const int N=510,INF=1e9+10;

int n,m;
int trie[N][10],cnt,c[N];
char s[N];
int dp[N][N][12];
int F[N][12],G[12],dep[N];

void dfs(int u) {
for(int v:trie[u]) if(v) dep[v]=dep[u]+1,dfs(v);
memset(F,63,sizeof F);
rep(i,0,dep[u]) F[i][0]=c[u]*(dep[u]-i);
for(int v:trie[u]) if(v) {
rep(j,0,dep[u]) {
rep(k,0,m) G[k]=F[j][k],F[j][k]=INF;
rep(k,0,m) rep(d,0,m-k) cmin(F[j][k+d],G[k]+dp[v][j][d]);
}
}
rep(d,0,dep[u]) {
rep(i,0,m) dp[u][d][i]=INF;
rep(i,0,m) {
cmin(dp[u][d][i+1],F[dep[u]][i]);
cmin(dp[u][d][i],F[d][i]);
}
}
}

int main(){
n=rd(),m=rd();
rep(i,1,n) {
scanf("%s",s+1);
int u=0;
for(int j=1;s[j];++j) {
int &v=trie[u][s[j]-'0'];
if(!v) v=++cnt;
u=v;
}
c[u]+=rd();
}
dfs(0);
int ans=INF;
rep(i,0,m) cmin(ans,dp[0][0][i]);
printf("%d\n",ans);
}

CF1111E - Tree

题目大意

给定一棵无根树 \(T\)\(q\) 次查询每次查询一个给定一个根 \(r\) ,点集 \(S\) 和限制 \(m\)

求将 \(S\) 分成不超过 \(m\) 个非空集合,使得最终每个集合内不存在两点为祖先关系


分析

容易发现题目是一个给定部分点集的树形 \(dp\) ,因此需要用虚树来处理

\(r\) 也加入虚树,从 \(r\) 开始 \(\text{dfs}\) 即确定了根为 \(r\)

dp部分

一种思路是树形背包,计算子树内分为 \(i\) 个集合的方案数,枚举在 \(\text{LCA}\) 处合并两个集合

但是由于要枚举合并的个数,难以写出优秀的复杂度

由于一条祖先链上点之间的集合独立,容易描述,因此可以考虑 \(\text{dfs}\) 序dp

按照 \(\text{dfs}\) 依次加入每一个点 \(u\) ,令 \(dp_i\) 表示当前有 \(i\) 个集合的方案数

则在 \(i\) 个集合中包含 \(dep_u\) 个集合 \(u\) 无法加入

枚举 \(i\) ,加入一个点 \(O(m)\) 转移,滚动数组即可

复杂度为 \(O(n\log n+\sum |S|\cdot m)\)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
const int N=1e5+10,P=1e9+7;

int n,m;
int L[N],top[N],son[N],sz[N],dfn,fa[N],dep[N];
struct Edge{
int to,nxt;
} e[N<<1];
int head[N],ecnt;
void AddEdge(int u,int v){
e[++ecnt]=(Edge){v,head[u]};
head[u]=ecnt;
}

void dfs(int u){
sz[u]=1;
for(int i=head[u];i;i=e[i].nxt) {
int v=e[i].to;
if(v==fa[u]) continue;
dep[v]=dep[fa[v]=u]+1,dfs(v);
if(sz[v]>sz[son[u]]) son[u]=v;
sz[u]+=sz[v];
}
}
void dfs(int u,int t){
top[u]=t,L[u]=++dfn;
if(son[u]) dfs(son[u],t);
for(int i=head[u];i;i=e[i].nxt) {
int v=e[i].to;
if(v==son[u] || v==fa[u]) continue;
dfs(v,v);
}
}
int LCA(int x,int y){
while(top[x]!=top[y]) dep[top[x]]>dep[top[y]]?x=fa[top[x]]:y=fa[top[y]];
return dep[x]<dep[y]?x:y;
}

vector <int> G[N];
int stk[N],T;
void Link(int u,int v){ G[u].pb(v),G[v].pb(u); }
void Ins(int u){
if(T<=1) return void(stk[++T]=u);
int lca=LCA(u,stk[T]);
if(lca==stk[T]) return void(stk[++T]=u);
while(T>1 && L[stk[T-1]]>=L[lca]) Link(stk[T],stk[T-1]),T--;
if(stk[T]!=lca) Link(stk[T],lca),stk[T]=lca;
stk[++T]=u;
}
int dis[N],mk[N];

int dp[310],a[N],c,rt;
void dfs_dp(int u,int f){
dis[u]=dis[f]+mk[u];
if(mk[u]) drep(i,m,0) dp[i]=((i?dp[i-1]:0)+1ll*(i-dis[f])*dp[i])%P;
for(int v:G[u]) if(v!=f) dfs_dp(v,u);
mk[u]=0,G[u].clear();
}

int main(){
n=rd(),m=rd();
rep(i,2,n){
int u=rd(),v=rd();
AddEdge(u,v),AddEdge(v,u);
}
dfs(1),dfs(1,1);
rep(_,1,m) {
c=rd(),m=rd(),rt=rd();
rep(i,1,c) mk[a[i]=rd()]=1;
a[++c]=1,a[++c]=rt;
sort(a+1,a+c+1,[&](int x,int y){ return L[x]<L[y]; });
T=0;
rep(i,1,c) if(a[i]!=a[i-1]) Ins(a[i]);
while(T>1) Link(stk[T-1],stk[T]),T--;
rep(i,0,m) dp[i]=0;
dp[0]=1;
dfs_dp(rt,0);
int ans=0;
rep(i,1,m) ans+=dp[i],Mod1(ans);
printf("%d\n",ans);
}
}

[HDU-6847] Decision (2020多校7T4) (类欧几里得问题)

枚举 \(|v_1-v_2|\) 后,可以递推,用含首项( \(v_1+v_2\) )的一次函数表示函数值为 \(a(v_1+v_2)+b\) ,则问题等价于求

\(\begin{aligned} \sum_{i=0}^n [2|(ai+b)\mod m] \end{aligned}\) ,其中 \(n\) 对于每个 \(v_1-v_2\) 是不同的

这个问题,可以转化为一个简单的类欧几里得问题

\((ai+b)\mod m \mod 2= ((ai+b)-(m\mod 2)\cdot \lfloor \frac{ai+b}{m}\rfloor )\mod 2\)

这个式子即把每次被 \(m\) 取模减少的 \(m\) 算进贡献

可以看到操作非常简单,可以直接套上万能欧几里得的板子

当然,也可以对于 \(m\) 的各种情况讨论,转化为求 \(\lfloor \frac{ai+b}{m}\rfloor\) ,其主要思想还有应用 \(x\mod 2=x-2\cdot \lfloor \frac{x}{2}\rfloor\) 的转化

我比赛时去抄了自己的类欧几里得模板

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
#define rep(i,a,b) for(int i=a,i##end=b;i<=i##end;++i)
#define drep(i,a,b) for(int i=a,i##end=b;i>=i##end;--i)

char IO;
template <class T=int> T rd(){
T s=0; int f=0;
while(!isdigit(IO=getchar())) if(IO=='-') f=1;
do s=(s<<1)+(s<<3)+(IO^'0');
while(isdigit(IO=getchar()));
return f?-s:s;
}

const int N=1e6+10;

int t,a,c,m;
int fx[N],fy[N];

ll gcd(ll a,ll b){ return b==0?a:gcd(b,a%b); }

ll D2(ll n){
return n*(n+1)/2;
}
ll F(ll a,ll b,ll c,ll n){
if(n<0) return 0;
if(a==0) return (b/c)*(n+1);
if(a>=c || b>=c) {
ll ans=F(a%c,b%c,c,n)+D2(n)*(a/c)+(n+1)*(b/c);
return ans;
}
ll t=(a*n+b)/c;
ll ans=t*n-F(c,-b+c-1,a,t-1);
return ans;
}

ll CalcMod(int a,int b,int m,int n){
return F(a,b,m,n)-F(a,b,m*2,n)*2;
}

ll Calc(int a,int b,int n){
// for i= [l,r] (ax+b) %m %2
if(~m&1){
// (ax+b)%2;
a%=2,b%=2;
if(a==0) return b*(n+1);
return (n+1+b)/2;
} else {
// (ax+b) %m %2 = ((ax+b)/m + ax+b)%2
int c=a%2,d=b%2;
if(c==0) {
// =((ax+b)/m+b)%2;
if(d==0) return CalcMod(a,b,m,n);
else return (n+1)-CalcMod(a,b,m,n);
} else {
// (ax+b) %m %2 = ((ax+b)/m + (x&1)+d)%2
ll ans=0;

// i%2 == 0
ll t=CalcMod(a*2,b,m,n/2);
if(d) t=n/2+1-t;
ans+=t;

b+=a;
// i%2 == 1
n--;
if(n<0) return ans;
t=CalcMod(a*2,b,m,n/2);
if(!d) t=n/2+1-t;
ans+=t;
return ans;
}
}
}

int main(){
rep(kase,1,rd()) {
t=rd(),a=rd(),c=rd(),m=rd();
fx[0]=1,fy[0]=0;
rep(i,1,t) fx[i]=1ll*fx[i-1]*a%m,fy[i]=(1ll*fy[i-1]*a+c)%m;
ll ans=0;
rep(i,0,t) {
if(i==0) {
ans+=Calc(fx[i]*2%m,fy[i],t);
} else {
// a<b, b - a = i
// a=[0,m-i]
// b=[i,m]
ll tmp=(1ll*i*fx[i]+fy[i])%m;
ans+=Calc(fx[i]*2%m,tmp,t-i)*2;
}
}
ll tmp=1ll*(t+1)*(t+1);
ans=tmp-ans;
ll g=gcd(tmp,ans);
printf("%lld/%lld\n",ans/g,tmp/g);
}
}

CF1051G - Distinctification

题目大意

对于一个二元组集合 \(\{(a_i,b_i)\}\)

每次可以进行操作

1.如果存在 \(a_i=a_j\) ,可以花费 \(b_i\) 代价 \(a_i\) 增加1

2.如果存在 \(a_i=a_{j}+1\) ,可以花费 \(-b_i\) 代价使 \(a_i\) 减少1

现在依次向集合插入 \(n\) 个二元组,求在所有时刻,对于当前的集合进行操作

最终使得不存在 \(a_i=a_j\) 时的最小花费(可以为负)


分析

容易发现对于给定的 \(a_i\) 集合,最终 \(a_i\) 的集合唯一固定

具体的,每次插入一个数值 \(x\) ,如果出现重复就会不停将 \(x\) 向后推推推

而事实上答案为 \(\sum b_i\cdot (a'_i-a_i)\) ,那么只需要最小化 \(\sum b_ia'_i\)

容易发现在任意时刻,如果 \([L,R]\) 内所有 \(a_i\) 都出现,就可以任意交换他们的 \(b_i\)

那么最终状态中每一个 \(a_i\) 连通块内,按照 \(b_i\) 从大到小排序即可

每次插入一个元素维护连通块之间的合并以及求出 \(\sum b_ia'_i\) 即可

可以用启发式合并/线段树合并维护

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
const int N=4e5+10,M=N*19,INF=1e9+10;

int n;
int ls[M],rs[M],c[M],cnt;
ll s[M],ans[M];
ll Ans;

int F[N],rt[N];
int Find(int x){ return F[x]==x?x:F[x]=Find(F[x]); }
void Up(int x){
c[x]=c[ls[x]]+c[rs[x]],s[x]=s[ls[x]]+s[rs[x]];
ans[x]=ans[ls[x]]+ans[rs[x]]+c[rs[x]]*s[ls[x]];
}
void Upd(int &p,int l,int r,int x){
if(!p) p=++cnt;
if(l==r) { c[p]=1,s[p]=x; return; }
int mid=(l+r)>>1;
x<=mid?Upd(ls[p],l,mid,x):Upd(rs[p],mid+1,r,x);
Up(p);
}
int Union(int x,int y,int l=1,int r=n){
if(!x||!y) return x|y;
int mid=(l+r)>>1;
ls[x]=Union(ls[x],ls[y],l,mid),rs[x]=Union(rs[x],rs[y],mid+1,r);
return Up(x),x;
}

void Add(int x,int k){
x=Find(x);
Ans+=k*(x*s[rt[x]]+ans[rt[x]]);
}

int main(){
n=rd();
rep(i,1,n){
int x=rd(),y=rd();
Ans-=1ll*x*y;
int f=Find(x);
if(!f) f=F[x]=x;
else Add(f,-1),F[f+c[rt[f]]]=f;
Upd(rt[f],1,n,y);
while(x=Find(x),y=Find(x-1)) {
Add(y,-1);
F[x]=x-1;
rt[y]=Union(rt[y],rt[x]);
}
while(x=Find(x),y=Find(x+c[rt[x]])) {
Add(y,-1);
F[x+c[rt[x]]]=x;
rt[x]=Union(rt[x],rt[y]);
}
Add(x,1),printf("%lld\n",Ans);
}
}

CF1067D - Computer Game

题目大意

给定 \(n\) 个操作,每个操作有1级和2级分别对应价值 \(a_i,b_i (a_i<b_i)\) ,初始每个操作为 \(1\)

每次操作 \(i\) ,有 \(p_i\) 概率操作成功,这会获得价值,并且获得一次升级的机会

\(t\) 次操作的期望最大权值和


分析

容易发现,当你拿到一次升级后,一定就一直选择 \(\max\{p_i\cdot b_i\}\) 进行操作

并且每次期望获得这么多的权值,不妨设 \(m=\max\{p_i\cdot b_i\}\)

需要决策的是拿到这个权值之前,选择哪一个操作

\(dp_i\) 表示有 \(i\) 次操作机会时的期望答案,则得到 \(dp\) 转移式子

\(\displaystyle dp_i=\max_{j\leq n}\{ p_j((i-1)m+a_j)+(1-p_j)dp_{i-1} \}\)

考虑这是一个斜率优化的形式,我们做一下变形

\(dp_i=\max\{ p_j((i-1)m-dp_{i-1}) +dp_{i-1}+p_ja_j\}\)

可以看到斜率优化与 \((i-1)m-dp_{i-1}\) 有关,设 \(g_i=im-dp_{i}\)

则问题可以转化为求 \((g_t)_{\min}\)

转化之后得到 \(g_i\) 的转移式

\(\displaystyle g_{i}=\min_{j\leq n} \{g_{i-1} (1-p_j)+m-a_jp_j\}\)

容易发现这样的转移相较于 \(dp_i\) ,脱离了 \(im\) 的问题

不妨对于 \((1-p_j,m-a_jp_j)\) 建立凸包,则转移可以斜率优化

现在考虑 \(g_i\) 的单调性,根据定义式我们可以感性地理解 \(g_i\) 是递增的(因为 \(dp_i\) 增率不可能超过 \(m\)

(事实上我并不知道怎么根据转移式说明它是单调的)

既然是 \(g_i\) 是单调,那么转移在凸包上的位置也是单调的,因此可以直接在凸包上移动决策点

倍增|二分完成操作

复杂度为 \(O(n(\log n+\log t))\)\(O(n(\log n+\log^2 t))\)

吐槽

Codeforces上写double ,long double真的磨人

还有一群疯子构造数据把两个点的 \(x_i,y_i\) 相差 \(10^{-9}\)

什么玩意儿,搞得我把 \(\text{eps}\) 开成 \(10^{-20}\)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
const int N=1e5+10,INF=1e9+10;
const db eps=1e-20;

int n; ll k;
db m;
struct Node{
db x,y;
Node(db x=0,db y=0):x(x),y(y){}
bool operator < (const Node __) const { return x<__.x-eps || (fabs(x-__.x)<eps && y>__.y); }
Node operator - (const Node _) const { return Node(x-_.x,y-_.y); }
db operator ^ (const Node _) const { return x*_.y-y*_.x; }
db operator [] (const db k){ return k*x+y; }
Node operator * (const Node _) {
return Node(x*_.x,_.x*y+_.y);
}
} A[N],S[N],Pow[35];
db Cross(Node a,Node b){ return (b.y-a.y)/(a.x-b.x); }
int T;

int main(){
n=rd(),k=rd<ll>();
rep(i,1,n) {
db a=rd(),b=rd(),p;scanf("%lf",&p);
cmax(m,p*b),A[i]=Node(1-p,a*p);
}
rep(i,1,n) A[i].y=m-A[i].y;
sort(A+1,A+n+1);
rep(i,1,n) {
while(T>1 && ((A[i]-S[T])^(S[T-1]-S[T]))<eps) T--;
S[++T]=A[i];
}
db g=0,s=k*m;
S[0].y=1e40;
while(k) {
while(T>1 && S[T-1][g]<=S[T][g]+eps) T--;
Pow[0]=S[T];
for(int i=1;(1ll<<i)<=k;++i) Pow[i]=Pow[i-1]*Pow[i-1];
drep(i,34,0) if((1ll<<i)<=k && S[T-1][Pow[i][g]]>=S[T][Pow[i][g]]+eps) k-=1ll<<i,g=Pow[i][g];
if(k && S[T-1][g]>=S[T][g]+eps) k--,g=Pow[0][g];
}
printf("%.10lf\n",s-g);
}

CF1119F - Niyaz and Small Degrees

题目大意

给定一棵带权树,对于每个 \(k\in[0,n-1]\)

求出删除一个权值最小的边集使得没有一个点度数 \(>k\)


分析

单个 \(k\)

考虑对于单个 \(k\) 的计算,可以有如下 \(O(n)\)\(dp\) 做法

\(dp_{u,0/1}\) 表示对于 \(u\) 子树内的点已经确定合法, \(0/1\) 表示连向父亲的边是否断掉

实际上我们需要在连向儿子的边中选择若干条断掉

也就是取若干 \(v\in son_u,dp_{v,1}\) 和若干 \(dp_{v,0}\)

实际上只需要考虑 \(dp_{v,1}-dp_{v,0}\) 差值的前 \(d\) 小,其中 \(d=deg_u-k(-1)\)

std::nth_element即可实现 \(O(n)\) 计算


所有 \(k\)

考虑对于 \(k\) ,只有 \(deg_u>k\)\(u\) 才会考虑对于它周围的边删除

我们称对于 \(k\) 这样的节点 \(u\) 为关键点,其集合为 \(S_k\)

换句话说, \(u\) 只有在 \(k\in[0,deg_u-1]\)\(deg_u\)\(k\) 中被计算答案

而又知道 \(\sum deg_u=2n-2\) ,故对于所有 \(k\) 考虑关键点的数量之和为 \(O(n)\)

但是实际实现上,因为这样的 \(u\) 构成若干联通子图,且那些与关键点所连的非关键点需要处理边的贡献

显然一个非关键点 \(dp_{v,0}=0,dp_{v,1}=w_v\)\(w_v\) 为父边权值

我们需要在 \(\{w_v|v\in son_u-S_k\}\)\(\{dp_{v,1}-dp_{v,0}|v\in son_u\cap S_k\}\) 中取前 \(d\) 小,可以用线段树 \(O(n\log n)\) 维护

如果使用基数排序+链表+懒标记做删除和 \(k\) 大操作,查询 \(k\) 大部分复杂度为 \(O(n)\) ,但是还是要sort \(dp_{v,1}-dp_{v,0}\)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
const int N=2.5e5+10,U=1e6,M=N*40;

int n,m;
int A[N],C,D[N];
vector <Pii> E[N],G[N];
int I[N],V[N];

int rt[N],ls[M],rs[M],c[M],cnt; ll s[M];
void Upd(int &p,int l,int r,int x,int y) {
if(!p) p=++cnt;
s[p]+=x*y,c[p]+=y;
if(l==r) return;
int mid=(l+r)>>1;
x<=mid?Upd(ls[p],l,mid,x,y):Upd(rs[p],mid+1,r,x,y);
}
ll Que(int p,int l,int r,int k){
if(c[p]<k) return 1e18;
if(!k || l==r) return l*k;
int mid=(l+r)>>1;
if(c[ls[p]]>=k) return Que(ls[p],l,mid,k);
return s[ls[p]]+Que(rs[p],mid+1,r,k-c[ls[p]]);
}

ll dp[N][2];
int vis[N];
void dfs(int u) {
vis[u]=1;
dp[u][0]=dp[u][1]=0;
vector <ll> val;
int c=0; ll s=0;
for(Pii t:G[u]) {
int v=t.first,w=t.second;
if(vis[v]) continue;
dfs(v);
dp[v][1]+=w,s+=dp[v][0];
if(dp[v][1]<=dp[v][0]) c++,s+=dp[v][1]-dp[v][0];
else val.pb(dp[v][1]-dp[v][0]);
}
sort(val.begin(),val.end());
// Get the sum of first k elements
auto F=[&](int k) {
ll ans=Que(rt[u],1,U,k),s=0;
for(ll i:val) {
if(--k<0) break;
cmin(ans,(s+=i)+Que(rt[u],1,U,k));
}
return ans;
};
dp[u][0]=s+F(max(0,D[u]-c-m));
dp[u][1]=s+F(max(0,D[u]-c-1-m));
//cout<<"Dfs "<<u<<' '<<dp[u][0]<<' '<<dp[u][1]<<endl;
}

ll ans[N];
int main() {
n=rd();
rep(i,2,n) {
int u=rd(),v=rd(),w=rd();
ans[0]+=w;
E[u].pb(mp(v,w)),E[v].pb(mp(u,w));
Upd(rt[u],1,U,w,1),Upd(rt[v],1,U,w,1);
D[u]++,D[v]++;
}
rep(i,1,n) I[i]=i;
sort(I+1,I+n+1,[&](int x,int y){ return D[x]>D[y]; });
int p=1;
for(m=n-1;m;m--) {
//printf("Solving %d \n",m);
while(p && D[I[p]]>m) {
int u=I[p++];
V[A[++C]=u]=1;
for(Pii v:E[u]) if(V[v.first]) {
Upd(rt[u],1,U,v.second,-1),Upd(rt[v.first],1,U,v.second,-1);
G[u].pb(v),G[v.first].pb(mp(u,v.second));
}
}
rep(i,1,C) vis[A[i]]=0;
rep(i,1,C) if(!vis[A[i]]) dfs(A[i]),ans[m]+=dp[A[i]][0];
}
rep(i,0,n-1) printf("%lld ",ans[i]);
}





CF1146G - Satanic Panic

题目大意

给定平面上 \(n\) 个点,求能够构成五角星的五元组数目


分析

实际上就是求五元凸包数目,下面直接考虑 \(k\) 元的形式

考虑凸包的两种判定方法:

1.所有转角 \(<\pi\)

然而这并不好实现

2.一个凸包可以根据 \(x_i\) 最大、最小的两个点分成上下两部分,上下分别判断转角 \(<\pi\)

(可以通过随机旋转规避 \(x_i\) 相同的情况)

(这题 \(k\) 较小,或许可以上下暴力枚举?)

一般的情况,枚举 \(x_i\) 最小的点 \(A\)

然后令 \(dp_{i,j}\) 表示当前凸包最后一条折线为 \((i,j)\) ,然后不断接新点

最后合并上下两个凸包

直接实现,单次 \(dp\) 状态 \(O(n^2k)\) ,转移 \(O(n)\) ,复杂度为 \(O(n^4k)\)

优化:

\((i,j)\rightarrow (j,k)\) ,可以先确定 \(j\)

然后按照转角顺序枚举 \(k\) ,双指针完成所有 \(k\) 的转移

如果预处理每个点出发的转角序,则预处理 \(O(n^2\log n)\) ,总复杂度为 \(O(n^3k)\)

(Code消失了)

0%