dp 练习 2024.4.9/4.10
Luogu8389 [ZJOI2022] 树#
考虑容斥,每个点有三个状态:不钦定、钦定都是叶子、钦定都是非叶子。对于每一种钦定状态都计算答案,然后乘上对应容斥系数即可。
叶子的钦定是可做的,考虑非叶子的钦定,我们可以用“容斥套容斥”的思想理解:对于钦定都是非叶子的点
把非叶子的钦定放回原容斥里描述:减去总方案数,加上钦定第一棵树中是叶子的方案数、加上钦定第二棵树中是叶子的方案数、减去两棵树中都是叶子的方案数。
和不钦定、钦定叶子的方案加起来,相当于:加上钦定第一棵树中是叶子的方案数、加上钦定第二棵树中是叶子的方案数、减去
设
转移容易。
点击查看代码
#include<bits/stdc++.h>
#define ll long long
#define pir pair<ll,ll>
#define mkp make_pair
#define fi first
#define se second
#define pb push_back
using namespace std;
const ll maxn=510, M=1e9;
ll n,mod,f[2][maxn][maxn],ans;
int main(){
scanf("%lld%lld",&n,&mod);
for(ll i=1;i<n;i++) f[1][1][i]=i;
puts("1");
for(ll i=2;i<n;i++){
memset(f[i&1],0,sizeof f[i&1]);
for(ll j=1;j<=n;j++)
for(ll k=1;k<=n;k++){
if(!f[i&1^1][j][k]) continue;
f[i&1][j][k-1]=(f[i&1][j][k-1]+f[i&1^1][j][k]*j*(k-1))%mod;
f[i&1][j+1][k]=(f[i&1][j+1][k]+f[i&1^1][j][k]*j*k)%mod;
f[i&1][j][k]=(f[i&1][j][k]-2*f[i&1^1][j][k]*j*k)%mod;
}
ans=0;
for(ll j=1;j<=i;j++)
ans=(ans+f[i&1][j][1]*j)%mod;
printf("%lld\n",(ans+mod)%mod);
}
return 0;
}
AGC036F Square Constraints#
相当于有两个圆,每个位置的取值必须在两个圆中间,求方案数。
本质上还是
如果有下面的圆,因为钦定后本质上还是在一段前缀上取值,我们考虑容斥。
钦定取值在下面圆内的位置集合
但是这样有个问题,我们难以计算方案数。尝试先排序:对于
对于未被钦定的位置
所以我们考虑 dp 开始前先枚举钦定的位置个数,便于计算。
点击查看代码
#include<bits/stdc++.h>
#define ll long long
#define pir pair<ll,ll>
#define mkp make_pair
#define fi first
#define se second
#define pb push_back
using namespace std;
const ll maxn=510;
ll n,id[maxn],mod,f[maxn][maxn],ans,l[maxn],r[maxn];
bool cmp(ll x,ll y){ ll p=(r[x]<r[y]);
x=(x<n? l[x]-1:r[x]), y=(y<n? l[y]-1:r[y]);
return x^y? x<y:p;
}
int main(){
scanf("%lld%lld",&n,&mod);
for(ll i=0;i<(n<<1);i++){
if(i<n) l[i]=ceil(sqrt(n*n-i*i));
r[i]=sqrt(4*n*n-i*i); id[i+1]=i;
r[i]=min(r[i],(n<<1)-1);
} sort(id+1,id+1+(n<<1),cmp);
for(ll d=0;d<=n;d++){ //printf("d = %lld\n",d);
memset(f,0,sizeof f);
f[0][0]=1; ll cnt=0;
for(ll i=1;i<=(n<<1);i++){
for(ll j=0;j<=d&&j<=i-1-cnt;j++){
if(id[i]>=n){
if(cnt+j<=r[id[i]]) f[i][j]=(f[i][j]+f[i-1][j]*(r[id[i]]+1-cnt-j))%mod;
}
else{
if(n+d+(i-1-cnt-j)<=r[id[i]]) f[i][j]=(f[i][j]+f[i-1][j]*(r[id[i]]+1-n-d-(i-1-cnt-j)))%mod;
if(j<d&&cnt+j<l[id[i]]) f[i][j+1]=(f[i][j+1]+f[i-1][j]*(l[id[i]]-cnt-j))%mod;
}
} cnt+=(id[i]>=n);
}
ans=(ans+(d&1? mod-1:1)*f[n<<1][d])%mod;
} printf("%lld",ans);
return 0;
}
AGC040E Prefix Suffix Addition#
考虑原问题的本质:将
挖掘问题的本质很重要,这样能转化模型。
我们暴力,设
转移很显然:
- 一个 trick:注意每次转移最多加
,并且都是全局贡献,那么 最多 段,直接维护。
设
接着处理
剩下那段是前缀,等于
顺便可以发现,我们的
点击查看代码
#include<bits/stdc++.h>
#define ll long long
#define pir pair<ll,ll>
#define mkp make_pair
#define fi first
#define se second
#define pb push_back
using namespace std;
const ll maxn=2e5+10, mod=998244353, iv=mod-mod/2;
ll n,a[maxn],p1,p2,p3,val;
int main(){
scanf("%lld",&n);
p1=p2=p3=val=0;
for(ll i=1;i<=n+1;i++){
if(i<=n) scanf("%lld",a+i);
ll q3=min(a[i]+1,max(p3,a[i]-(a[i-1]-p3))),
q2=max(0ll,min(min(p3,a[i]-(a[i-1]-p3)),max(p2,a[i]-(a[i-1]-p2)))), q1=0, v=val;
if(q3>a[i]) q3=q2, q2=q1, q1=0, ++v;
p1=q1, p2=q2, p3=q3, val=v;
} printf("%lld",val);
return 0;
}
AGC022F Checkers#
详见 AGC022F 做题记录
CF1930G Prefix Max Set Counting#
直接 DP 最终序列,然后思考充要条件。
设
设
否则思考上一个点
如果不是,
思考还有什么条件,设
这个比较显然,我们一定是先 dfs 完那棵子树再出来的。
然后我们会发现这些转移也是充分的,在这种条件下,我们不可能重复走进同一棵子树。
所以可以直接 DP。首先,
然后思考
这样一来,我们只需要求
点击查看代码
#include<bits/stdc++.h>
#define ll long long
#define pir pair<ll,ll>
#define mkp make_pair
#define fi first
#define se second
#define pb push_back
using namespace std;
const ll maxn=1e6+10, mod=998244353;
ll t,n,fa[maxn],mx[maxn],pre[maxn],tree[maxn],dfn[maxn],out[maxn],ti,f[maxn];
vector<ll>to[maxn],vec[maxn];
void dfs(ll u,ll ff){
dfn[u]=++ti;
fa[u]=ff; pre[u]=max(pre[ff],u);
mx[u]=u;
for(ll v:to[u])
if(v!=ff) dfs(v,u), mx[u]=max(mx[u],mx[v]);
out[u]=ti;
}
void add(ll x,ll v){
while(x<=n){
tree[x]=(tree[x]+v)%mod;
x+=x&-x;
}
}
ll ask(ll x){
ll v=0;
while(x){
v=(v+tree[x])%mod;
x&=x-1;
} return v;
}
int main(){
scanf("%lld",&t);
while(t--){
scanf("%lld",&n);
for(ll i=1;i<n;i++){
ll u,v; scanf("%lld%lld",&u,&v);
to[u].pb(v), to[v].pb(u);
}
dfs(1,0);
f[1]=1;
for(ll i=2;i<=n;i++)
if(pre[fa[i]]<i) vec[pre[fa[i]]].pb(i);
for(ll u=2;u<=n;u++){
if(pre[fa[u]]>u) continue;
f[u]+=f[pre[fa[u]]], f[u]%=mod;
f[u]=(f[u]+ask(dfn[u]))%mod;
for(ll x:vec[u]) f[x]=mod-ask(dfn[x]);
ll x=u;
while(x>1&&mx[x]==u){
add(dfn[fa[x]],f[u]), add(out[fa[x]]+1,mod-f[u]);
x=fa[x];
}
}
printf("%lld\n",f[n]);
for(ll i=1;i<=n;i++){
to[i].clear(), vec[i].clear();
tree[i]=f[i]=0;
} ti=0;
}
return 0;
}
AGC020F Arcs on a Circle#
直接做很困难。
注意到所有弧长都是整数,他们的起始位置的小数部分期望下分别为
我们考虑断环为链,这里有个比较技巧的东西,以最长弧的起点断开,这样前面的弧一定包含不到这条弧,那么最后一个位置延伸过去的弧就不用管。
这样弧就变成了线段。由于每个线段的起点的小数部分已经确定,我们把
直接 dp,设
转移是容易的。
点击查看代码
#include<bits/stdc++.h>
#define ll long long
#define pir pair<ll,ll>
#define mkp make_pair
#define fi first
#define se second
#define pb push_back
using namespace std;
const ll maxn=52, mod=1e9+7;
ll N,C,L[maxn],p[maxn];
double f[310][310][1<<6],ans;
int main(){
scanf("%lld%lld",&N,&C);
for(ll i=1;i<=N;i++) scanf("%lld",L+i);
sort(L+1,L+1+N);
for(ll i=2;i<=N;i++) p[i]=i-1;
do{
memset(f,0,sizeof f);
f[1][L[N]*N+1][0]=1;
for(ll i=2;i<=N*C;i++){
for(ll j=i;j<=N*C+1;j++){
for(ll S=0;S<(1<<N-1);S++){
if(i<j) f[i][j][S]+=f[i-1][j][S];
ll x=p[(i-1)%N+1];
if(x&&!(S&(1<<x-1))){
f[i][min(N*C+1,max(j,i+L[x]*N))][S|(1<<x-1)]+=f[i-1][j][S]/C;
}
}
}
}
ans+=f[N*C][N*C+1][(1<<N-1)-1];
}while(next_permutation(p+2,p+N+1));
for(ll i=1;i<N;i++) ans/=i;
printf("%.15lf",ans);
return 0;
}
CF1466H Finding satisfactory solutions#
详见 CF1466H 做题记录
Luogu4426 [HNOI/AHOI2018] 毒瘤#
终于是一道比较 trival 的题了。
考虑广义串并联图的做法,会发现在此处仍然使用:
-
删一度点
-
缩二度点
每删掉一个点后,会带走一条边,所以最终是形如
最终每个点度数都
所以可以直接暴力枚举每个点的状态。
点击查看代码
#include<bits/stdc++.h>
#define ll long long
#define pir pair<ll,ll>
#define mkp make_pair
#define fi first
#define se second
#define pb push_back
using namespace std;
const ll maxn=2e5+10, mod=998244353;
struct Data {ll dp[2][2];};
set<ll>to[maxn];
unordered_map<ll,Data>w[maxn];
ll n,m,q[maxn],l,r,ans,g[maxn][2],vis[maxn],id[maxn],rk[maxn],len;
void ins(ll u,ll v,Data t){
if(to[u].count(v)){
Data p=w[u][v];
p.dp[0][0]=p.dp[0][0]*t.dp[0][0]%mod;
p.dp[0][1]=p.dp[0][1]*t.dp[0][1]%mod;
p.dp[1][0]=p.dp[1][0]*t.dp[1][0]%mod;
p.dp[1][1]=p.dp[1][1]*t.dp[1][1]%mod;
w[u][v]=p;
swap(p.dp[0][1],p.dp[1][0]), w[v][u]=p;
} else{
to[u].insert(v), to[v].insert(u);
w[u][v]=t;
swap(t.dp[0][1],t.dp[1][0]), w[v][u]=t;
}
}
int main(){
scanf("%lld%lld",&n,&m);
for(ll i=1;i<=m;i++){
ll u,v; scanf("%lld%lld",&u,&v);
to[u].insert(v), to[v].insert(u);
w[u][v]=w[v][u]=(Data){{{1,1},{1,0}}};
}
for(ll i=1;i<=n;i++) g[i][0]=g[i][1]=1;
for(ll i=1;i<=n;i++)
if(to[i].size()<=2) q[++r]=i, vis[i]=1;
l=1; vis[q[n]]=0, r=min(r,n-1);
while(l<=r){
ll u=q[l++];
if(to[u].size()==1){
ll v=*to[u].begin(); to[u].clear(), to[v].erase(u);
Data tmp=w[u][v];
g[v][0]=(g[u][0]*tmp.dp[0][0]+g[u][1]*tmp.dp[1][0])%mod*g[v][0]%mod;
g[v][1]=(g[u][0]*tmp.dp[0][1]+g[u][1]*tmp.dp[1][1])%mod*g[v][1]%mod;
if(r+1<n&&to[v].size()<=2&&!vis[v]) q[++r]=v, vis[v]=1;
} else{
ll v1=*to[u].begin(), v2=*to[u].rbegin();
to[u].clear(), to[v1].erase(u), to[v2].erase(u);
Data t1=w[v1][u], t2=w[u][v2];
Data t;
t.dp[0][0]=(t1.dp[0][0]*t2.dp[0][0]%mod*g[u][0]+t1.dp[0][1]*t2.dp[1][0]%mod*g[u][1])%mod;
t.dp[0][1]=(t1.dp[0][0]*t2.dp[0][1]%mod*g[u][0]+t1.dp[0][1]*t2.dp[1][1]%mod*g[u][1])%mod;
t.dp[1][0]=(t1.dp[1][0]*t2.dp[0][0]%mod*g[u][0]+t1.dp[1][1]*t2.dp[1][0]%mod*g[u][1])%mod;
t.dp[1][1]=(t1.dp[1][0]*t2.dp[0][1]%mod*g[u][0]+t1.dp[1][1]*t2.dp[1][1]%mod*g[u][1])%mod;
ins(v1,v2,t);
}
}
for(ll i=1;i<=n;i++)
if(!vis[i]) id[++len]=i, rk[i]=len;
for(ll S=0;S<(1<<len);S++){ ll res=1;
for(ll i=1;i<=len;i++){
ll u=id[i]; res=res*g[u][(S>>i-1)&1]%mod;
for(set<ll>::iterator it=to[u].begin();it!=to[u].end();it++){
ll v=*it;
if(v>u) res=res*w[u][v].dp[(S>>i-1)&1][(S>>rk[v]-1)&1]%mod;
}
} ans=(ans+res)%mod;
} printf("%lld",ans);
return 0;
}
AGC012F Prefix Median#
AGC,考虑充要条件,直接用充要条件计数。
先假设
不妨先对
我们不妨倒着进行整个过程,对于
如果前
这下结论很好猜了:对于
把充要条件结合图像:一开始对于
这个 dp 是容易的,设
考虑
因此,当新拓展的黑点数值和原来有的黑点一样时,拓展是不必要的,直接 dp 也是容易的。
点击查看代码
#include<bits/stdc++.h>
#define ll long long
#define pir pair<ll,ll>
#define mkp make_pair
#define fi first
#define se second
#define pb push_back
using namespace std;
const ll maxn=110, mod=1e9+7;
ll n,a[maxn],f[maxn][maxn][maxn],b[maxn],ans;
void add(ll &x,const ll &y) {x=(x+y>=mod? x+y-mod:x+y);}
int main(){
scanf("%lld",&n);
for(ll i=1;i<(n<<1);i++){
scanf("%lld",a+i);
} sort(a+1,a+(n<<1));
b[a[n]]=1; ll cnt=1;
f[n][1][1]=1;
for(ll i=n-1;i;i--){
ll le=(!b[a[n-(n-i)]]), ri=(!b[a[n+(n-i)]]);
b[a[n-(n-i)]]=b[a[n+(n-i)]]=1;
for(ll j=1;j<=cnt;j++)
for(ll k=1;k<=j;k++){
if(le) add(f[i][j+ri+le-(k-1)][1],f[i+1][j][k]);
if(ri) add(f[i][k+le+ri][k+le+ri],f[i+1][j][k]);
add(f[i][j+le+ri][k+le],f[i+1][j][k]);
for(ll x=1;x<=j;x++)
if(x<k) add(f[i][x+(j-k+1)+le+ri][x+le],f[i+1][j][k]);
else if(x>k) add(f[i][k+(j-x+1)+le+ri][k+1+le],f[i+1][j][k]);
}
cnt+=le+ri;
}
for(ll i=1;i<=cnt;i++)
for(ll j=1;j<=i;j++) add(ans,f[1][i][j]);
printf("%lld",ans);
return 0;
}
AGC041F Histogram Rooks#
思考容斥。
钦定
不妨直接考虑钦定了哪些行和列不能放车,设两个集合分别为
后者不好做,考虑二次容斥,钦定一些行和列不被覆盖。
下面称 “二次钦定集合” 为不被二次容斥钦定不被选择的格子覆盖的行和列,“一次钦定集合” 为钦定的不能放车的行和列。
设二次钦定集合为
那么贡献为:
注意后面的求和式,等价于
因此我们只需要关心如何求
设
转移先合并左右两边,然后枚举
点击查看代码
#include<bits/stdc++.h>
#define ll long long
#define pir pair<ll,ll>
#define mkp make_pair
#define fi first
#define se second
#define pb push_back
using namespace std;
const ll maxn=410, mod=998244353;
ll n,a[maxn],lc[maxn],rc[maxn],stk[maxn],top,f[maxn][maxn][2],rt,g[maxn][2],pw[maxn*maxn],ans;
ll fac[maxn],ifac[maxn];
ll C(ll n,ll m) {return n<m? 0:fac[n]*ifac[m]%mod*ifac[n-m]%mod;}
void dfs(ll u,ll l,ll r,ll ff){
if(lc[u]) dfs(lc[u],l,u-1,u);
if(rc[u]) dfs(rc[u],u+1,r,u);
memset(g,0,sizeof g);
for(ll i=0;i<=u-l;i++)
for(ll j=0;j<=r-u;j++)
g[i+j][0]=(g[i+j][0]+f[lc[u]][i][0]*f[rc[u]][j][0])%mod,
g[i+j][1]=(g[i+j][1]+f[lc[u]][i][1]*f[rc[u]][j][1]
+f[lc[u]][i][1]*f[rc[u]][j][0]+f[lc[u]][i][0]*f[rc[u]][j][1])%mod;
for(ll i=r-l;~i;i--){
g[i+1][0]=(g[i+1][0]+mod-g[i][0])%mod, g[i+1][1]=(g[i+1][1]+g[i][0])%mod;
} ll d=a[u]-a[ff];
for(ll i=0;i<=r-l+1;i++){
for(ll j=0;j<=d;j++){
f[u][i][1]=(f[u][i][1]+g[i][1]*(j&1? mod-1:1)%mod
*pw[(r-l+1-i)*(d-j)]%mod*C(d,j))%mod;
for(ll k=0;j+k<=d;k++){
f[u][i][0]=(f[u][i][0]+g[i][0]*(j&1? mod-1:1)%mod
*pw[(r-l+1-i)*(d-j-k)]%mod*C(d,j)%mod*C(d-j,k))%mod;
}
}
}
}
ll power(ll a,ll b=mod-2){
ll s=1;
while(b){
if(b&1) s=s*a%mod;
a=a*a%mod; b>>=1;
} return s;
}
int main(){
f[0][0][0]=1;
scanf("%lld",&n);
pw[0]=fac[0]=1;
for(ll i=1;i<=n;i++) fac[i]=fac[i-1]*i%mod;
ifac[n]=power(fac[n]);
for(ll i=n;i;i--) ifac[i-1]=ifac[i]*i%mod;
for(ll i=1;i<=n*n;i++) pw[i]=pw[i-1]*2%mod;
for(ll i=1;i<=n;i++){
scanf("%lld",a+i);
while(top&&a[stk[top]]>a[i]) lc[i]=stk[top--];
if(top) rc[stk[top]]=i;
stk[++top]=i;
}
rt=stk[1];
dfs(rt,1,n,0);
for(ll i=0;i<=n;i++)
ans=(ans+f[rt][i][0]+f[rt][i][1])%mod;
printf("%lld",ans);
return 0;
}
出处:https://www.cnblogs.com/Sktn0089/p/18124294
版权:本作品采用「署名-非商业性使用-相同方式共享 4.0 国际」许可协议进行许可。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 无需6万激活码!GitHub神秘组织3小时极速复刻Manus,手把手教你使用OpenManus搭建本
· C#/.NET/.NET Core优秀项目和框架2025年2月简报
· Manus爆火,是硬核还是营销?
· 一文读懂知识蒸馏
· 终于写完轮子一部分:tcp代理 了,记录一下