【UR #20】跳蚤电话

将加边变为加点,方案数为 \((n-1)!\) 除以一个数,\(dp\) 每种方案要除的数之和即可。

点击查看代码
#include<bits/stdc++.h>
using namespace std;
int n;
int ver[200005],ne[200005],head[100005],cnt;
inline void link(int x,int y){
ver[++cnt]=y;
ne[cnt]=head[x];
head[x]=cnt;
}
const int md=998244353;
int dp[100005],siz[100005],inv[100005];
void dfs(int x,int fi){
int res=1;siz[x]=1;
for(int i=head[x];i;i=ne[i]){
int u=ver[i];
if(u==fi)continue;
dfs(u,x);siz[x]+=siz[u];
res=1ll*res*dp[u]%md;
}
for(int i=head[x];i;i=ne[i]){
int u=ver[i];
if(u==fi)continue;
dp[x]=(dp[x]+1ll*res*siz[u]%md*inv[siz[x]]%md*inv[siz[x]-siz[u]])%md;
}
dp[x]=(dp[x]+1ll*res*inv[siz[x]])%md;
}
int main(){
scanf("%d",&n);
for(int i=1;i<n;i++){
int x,y;scanf("%d%d",&x,&y);
link(x,y);link(y,x);
}
inv[0]=inv[1]=1;
for(int i=2;i<=n;i++)inv[i]=1ll*(md-md/i)*inv[md%i]%md;
int ans=1;
for(int i=head[1];i;i=ne[i]){
int u=ver[i];
dfs(u,1);ans=1ll*ans*dp[u]%md;
}
for(int i=1;i<n;i++)ans=1ll*ans*i%md;
printf("%d\n",ans); return 0;
}

【UR #12】密码锁

发现一张竞赛图的强连通分量个数等于将这张图划分成两个集合使得集合之间的边方向相同的方案数 +1 ,因此可以枚举集合的划分,这里直接做的复杂度是 \(O(2^n)\) ,发现对于一个联通块射击到的点数最多为边数 +1 ,因此可以对特殊边所涉及到的每个联通块分别枚举划分,做一下背包即可。

点击查看代码
#include<bits/stdc++.h>
using namespace std;
const int md=998244353,inv2=(md+1)/2;
inline int pwr(int x,int y){
int res=1;
while(y){
if(y&1)res=1ll*res*x%md;
x=1ll*x*x%md;y>>=1;
}
return res;
}
int n,m;
int x,y,z;
int aw[2005],bw[2005],cw[2005];
vector<int> vec[45];
int a[45][45],stk[45],top;
bool vis[45];
void init(int x){
if(vis[x])return;
vis[x]=1;
stk[++top]=x;
for(auto u:vec[x])init(u);
}
int f[45],g[45],h[45];
int stk0[45],top0,stk1[45],top1;
void dfs(int pos){
if(pos>top){
int res=1;
for(int i=1;i<=top0;i++){
for(int j=1;j<=top1;j++)res=1ll*res*a[stk0[i]][stk1[j]]%md;
}
res=1ll*res*bw[top0*top1]%md;
g[top0]=(g[top0]+res)%md;
return;
}
stk0[++top0]=stk[pos];
dfs(pos+1);--top0;
stk1[++top1]=stk[pos];
dfs(pos+1);--top1;
}
inline void calc(){
for(int i=0;i<=n;i++)h[i]=f[i],f[i]=0;
for(int i=0;i<=n;i++){
for(int j=0;i+j<=n;j++)f[i+j]=(f[i+j]+1ll*h[i]*g[j])%md;
}
}
long long sum=0;
int main(){
scanf("%d%d",&n,&m);
for(int i=1;i<=n;i++){
for(int j=1;j<=n;j++)a[i][j]=5000;
}
aw[0]=1;for(int i=1;i<=n*n;i++)aw[i]=1ll*aw[i-1]*5000%md;
for(int i=0;i<=n*n;i++)bw[i]=pwr(aw[i],md-2);
cw[0]=1;for(int i=1;i<=n*n;i++)cw[i]=1ll*cw[i-1]*10000%md;
for(int i=1;i<=m;i++){
scanf("%d%d%d",&x,&y,&z);
a[x][y]=z;a[y][x]=10000-z;
vec[x].push_back(y);vec[y].push_back(x);
}
f[0]=1;
for(int i=1;i<=n;i++)if(!vis[i]){
top=0;init(i);
for(int j=0;j<=n;j++)g[j]=0;
dfs(1);calc();
}
int ans=0;
for(int i=1;i<n;i++)ans=(ans+1ll*f[i]*aw[i*(n-i)]%md*cw[n*(n-1)/2-i*(n-i)])%md;
ans=1ll*(ans+cw[n*(n-1)/2])%md*cw[n*(n-1)/2]%md;
printf("%d\n",(ans+md)%md); return 0;
}

【UR #17】滑稽树上滑稽果

显然,无论在什么情况下,最优解都是一条链,而且每个点的滑稽度不小于所有点的 \(\text{and}\) 之和,因此可以设 \(dp[s]\) 表示拉出一条链,使得链的最下面一个点的滑稽度为 \(s\) 的最小代价,暴力枚举转移是 \(O(n\times a)\) 的,实际上转移时只需要枚举子集,判断是否有点的权值与 \(S\) 与 \(T\) 的差集无交即可,时间复杂度 \(O(3^{\log a})\)

点击查看代码
#include<bits/stdc++.h>
using namespace std;
int n;
bool vis[1<<18];
const int lim=(1<<18)-1;
inline void fwt(){
for(int i=0;i<18;i++){
for(int s=0;s<(1<<18);s++){
if((s>>i)&1)continue;
vis[s|(1<<i)]|=vis[s];
}
}
}
long long dp[1<<18],tmp=lim;
int main(){
scanf("%d",&n);
for(int i=1;i<=n;i++){
int x;scanf("%d",&x);
vis[x]=1;tmp&=x;
}
fwt();
memset(dp,0x3f,sizeof(dp));
dp[lim]=n*tmp;
for(int s=lim;s;s--){
if((s&tmp)!=tmp)continue;
for(int u=(s&(s-1));1;u=(s&(u-1))){
int delta=((s-u)^lim);
if(vis[delta])dp[u]=min(dp[u],dp[s]+u-tmp);
if(!u)break;
}
}
printf("%lld\n",dp[tmp]); return 0;
}

【UNR #2】梦中的题面

CF1342F Make It Ascending

CF1239E Turtle

可以发现最优摆放方式一定是最小值和次小值一个放左上角一个放右下角,上面升序排列,下面倒序排列。最优行走路线要么将上面一行走完,要么将下面一行走完。

背包算出将最小值和次小值去除后的所有可能,取最优结果即可。

点击查看代码
#include<bits/stdc++.h>
using namespace std;
int n;
int a[55],sum;
bitset<1250005> dp[25];
int main(){
scanf("%d",&n);
for(int i=1;i<=n;i++)scanf("%d",&a[i]);
for(int i=1;i<=n;i++)scanf("%d",&a[i+n]);
sort(a+1,a+2*n+1);
dp[0][0]=1;
for(int i=3;i<=2*n;i++){
sum+=a[i];
for(int j=n-1;j;j--)dp[j]|=(dp[j-1]<<a[i]);
}
int ans=1e9,up=0;
for(int i=0;i<=sum;i++)if(dp[n-1][i]){
if(ans>max(a[1]+a[2]+i,a[1]+a[2]+sum-i)){
ans=max(a[1]+a[2]+i,a[1]+a[2]+sum-i);
up=i;
}
}
vector<int> UP,DOWN;
int tot=n-1;
for(int i=3;i<=2*n;i++){
if(tot&&dp[tot-1][up-a[i]]){
UP.push_back(a[i]);
up-=a[i];tot--;
}else DOWN.push_back(a[i]);
}
sort(UP.begin(),UP.end());reverse(UP.begin(),UP.end());
sort(DOWN.begin(),DOWN.end());
printf("%d ",a[1]);
for(auto it:DOWN)printf("%d ",it);puts("");
for(auto it:UP)printf("%d ",it);
printf("%d",a[2]); return 0;
}

CF1403C Chess Rush

CF613E Puzzle Lover

将路径拆成三个部分,两边哈希,中间 \(dp\) ,需要特判长度为 \(1\) 和 \(2\) 的部分。

点击查看代码
#include<bits/stdc++.h>
using namespace std;
int n,m;
char a[2][2005],s[2005];
unsigned long long le[2][2005],ri[2][2005],hshs[2005],bas[2005];
inline unsigned long long Get(unsigned long long *hsh,int l,int r){
if(l>r)return 0;
return hsh[r]-hsh[l-1]*bas[r-l+1];
}
inline unsigned long long rGet(unsigned long long *hsh,int l,int r){
if(l>r)return 0;
return hsh[l]-hsh[r+1]*bas[r-l+1];
}
inline void init(){
bas[0]=1;
for(int i=1;i<=max(n,m);i++)bas[i]=131*bas[i-1];
for(int i=1;i<=m;i++)hshs[i]=hshs[i-1]*131+s[i];
for(int i=1;i<=n;i++)le[0][i]=le[0][i-1]*131+a[0][i];
for(int i=1;i<=n;i++)le[1][i]=le[1][i-1]*131+a[1][i];
for(int i=n;i>=1;i--)ri[0][i]=ri[0][i+1]*131+a[0][i];
for(int i=n;i>=1;i--)ri[1][i]=ri[1][i+1]*131+a[1][i];
}
const int md=1e9+7;
int dp[2][2005][2005][2];
inline int calc(){
int res=0;init();
for(int i=0;i<=n;i++){
dp[0][i][0][0]=1;
for(int j=1;j<=m;j++)dp[0][i][j][0]=0;
for(int j=1;j<=m;j++)dp[0][i][j][1]=0;
dp[1][i][0][0]=1;
for(int j=1;j<=m;j++)dp[1][i][j][0]=0;
for(int j=1;j<=m;j++)dp[1][i][j][1]=0;
}
for(int i=1;i<=n;i++){
for(int j=2;j<=i&&2*j<=m;j++){
if(rGet(ri[0],i-j+1,i)*bas[j]+Get(le[1],i-j+1,i)==Get(hshs,1,2*j))dp[1][i][2*j][1]=1;
if(rGet(ri[1],i-j+1,i)*bas[j]+Get(le[0],i-j+1,i)==Get(hshs,1,2*j))dp[0][i][2*j][1]=1;
}
}
for(int i=1;i<=n;i++){
for(int j=1;j<=m;j++){
if(a[0][i]==s[j]){
dp[0][i][j][0]=(dp[0][i][j][0]+dp[0][i-1][j-1][0])%md;
dp[0][i][j][0]=(dp[0][i][j][0]+dp[0][i-1][j-1][1])%md;
if(j!=1)dp[0][i][j][1]=(dp[0][i][j][1]+dp[1][i][j-1][0])%md;
}
if(a[1][i]==s[j]){
dp[1][i][j][0]=(dp[1][i][j][0]+dp[1][i-1][j-1][0])%md;
dp[1][i][j][0]=(dp[1][i][j][0]+dp[1][i-1][j-1][1])%md;
if(j!=1)dp[1][i][j][1]=(dp[1][i][j][1]+dp[0][i][j-1][0])%md;
}
// cout<<i<<" "<<j<<": "<<endl;
// cout<<dp[0][i][j][0]<<" "<<dp[0][i][j][1]<<endl;
// cout<<dp[1][i][j][0]<<" "<<dp[1][i][j][1]<<endl;
if(((m-j)&1)||(m-j)/2>n-i||m-j==2)continue;
if(Get(le[0],i+1,i+(m-j)/2)*bas[(m-j)/2]+rGet(ri[1],i+1,i+(m-j)/2)==Get(hshs,j+1,m)){
// cout<<"calced 0"<<endl;
res=(res+dp[0][i][j][0])%md;
res=(res+dp[0][i][j][1])%md;
}
if(Get(le[1],i+1,i+(m-j)/2)*bas[(m-j)/2]+rGet(ri[0],i+1,i+(m-j)/2)==Get(hshs,j+1,m)){
// cout<<"calced 1"<<endl;
res=(res+dp[1][i][j][0])%md;
res=(res+dp[1][i][j][1])%md;
}
}
}
// puts("---");
return res;
}
int main(){
scanf("%s",a[0]+1);n=strlen(a[0]+1);
scanf("%s",a[1]+1);
scanf("%s",s+1);m=strlen(s+1);
int ans=0;
ans=calc();
reverse(a[0]+1,a[0]+n+1);
reverse(a[1]+1,a[1]+n+1);
ans=(ans+calc())%md;
if(m==1){
for(int i=1;i<=n;i++)if(a[0][i]==s[1])ans=(ans-1+md)%md;
for(int i=1;i<=n;i++)if(a[1][i]==s[1])ans=(ans-1+md)%md;
}
if(m==2){
for(int i=1;i<=n;i++)if(a[0][i]==s[1]&&a[1][i]==s[2])ans=(ans-1+md)%md;
for(int i=1;i<=n;i++)if(a[1][i]==s[1]&&a[0][i]==s[2])ans=(ans-1+md)%md;
}
printf("%d",ans); return 0;
}

CF1466H Finding satisfactory solutions

分析一下图的性质:

图中有若干个白色环(已确定)。然后连黑色边,使得所有的白色环形成一个 DAG ,并且一个点向 \(j\) 个点连黑色边的方案数为 \(j!(n-1-j)!\) 。

显然相同长度的白色环可以看做同一种。

设大小为 \(i\) 的白色环有 \(c_i\) 个,则不同的子图有 \(\prod(c_i+1)\) 个,这个数字不会超过 \(1440\) ,所以状态数很少。

然后考虑数 \(\text{DAG}\) 的个数,设 \(f(S)\) 为子图 \(S\) 构成 \(\text{DAG}\) 的方案数。

根据套路,枚举 \(S\) 的一个子图 \(T\) ,表示图中入度为 \(0\) 的点。将 \(T\) 连向 \(S-T\) ,此时还要满足 \(S-T\) 中的每一个点都要被连到,所以要容斥一下。

设 \(\mathrm{ways}(x,y)\) 表示 \(y\) 个点向 \(x\) 个点连边的方案数。

那么有转移 :

\[f_{S} = \sum_{T\subset S} f_{S-T} \times \mathrm{ways}(|S|-|T|,|T|)\times (-1)^{|T|+1}
\]
\[\mathrm{ways}(x,y) = (\mathrm{ways}(x,1))^y = (\sum_{i=0}^x \binom{x}{i} i!(n-i-1)!)^y
ways(x,y)=(ways(x,1))
\]

\(\mathrm{ways}\) 可以直接 \(O(n^2)\) 预处理,由于最多只有 \(1440\) 个状态,\(\text{DP}\) 可以直接枚举子图转移,复杂度 \(O(1440^2)\)

点击查看代码
#include<bits/stdc++.h>
using namespace std;
int n;
int a[45];
const int md=1e9+7;
bool vis[45];
int Getsiz(int x,int siz){
if(vis[x])return siz;
vis[x]=1;return Getsiz(a[x],siz+1);
}
int c[45];
int ways[45][45],fac[45],inv[45];
inline int C(int x,int y){
if(x<y)return 0;
return 1ll*fac[x]*inv[y]%md*inv[x-y]%md;
}
inline void init(){
fac[0]=fac[1]=inv[0]=inv[1]=1;
for(int i=2;i<=n;i++)fac[i]=1ll*fac[i-1]*i%md;
for(int i=2;i<=n;i++)inv[i]=1ll*(md-md/i)*inv[md%i]%md;
for(int i=2;i<=n;i++)inv[i]=1ll*inv[i]*inv[i-1]%md;
ways[0][0]=1;
for(int i=0;i<=n;i++){
for(int j=0;j<=i;j++)ways[1][i]=(ways[1][i]+1ll*C(i,j)*fac[j]%md*fac[n-1-j])%md;
for(int j=2;j<=n;j++)ways[j][i]=1ll*ways[j-1][i]*ways[1][i]%md;
}
}
int dp[2005],pre[45],nxt[45];
inline int rk(int *tmp){
int res=0;
for(int i=1;i<=n;i++)res=(c[i]+1)*res+tmp[i];
return res;
}
inline void translate(int *tmp,int x){
for(int i=n;i;i--){
tmp[i]=x%(c[i]+1);
x/=(c[i]+1);
}
}
int main(){
scanf("%d",&n);init();
for(int i=1;i<=n;i++)scanf("%d",&a[i]);
for(int i=1;i<=n;i++)if(!vis[i])c[Getsiz(i,0)]++;
dp[0]=1;
int lim=1;
for(int i=1;i<=n;i++)lim*=(c[i]+1);lim--;
for(int i=1;i<=lim;i++){
translate(nxt,i);
int tot0=0,tot1=0;
for(int j=1;j<=n;j++)tot0+=nxt[j],tot1+=nxt[j]*j;
for(int j=0;j<i;j++){
translate(pre,j);
int tmp0=0,tmp1=0,coe=1;
for(int j=1;j<=n;j++)tmp0+=pre[j],tmp1+=pre[j]*j;
for(int j=1;j<=n;j++)coe=1ll*coe*C(nxt[j],pre[j]);
if(!((tot0-tmp0)&1))coe*=-1;
dp[i]=(dp[i]+1ll*dp[j]*coe%md*ways[tot1-tmp1][tmp1])%md;
}
}
printf("%d\n",(dp[lim]+md)%md); return 0;
}

CF582D Number of Binominal Coefficients

令 \(v_p(n)\)(\(p\) 是质数,\(n\) 是正整数)表示 \(n\) 的标准分解式中的 \(p\) 的幂次。

发现 \(v_p(n!)=\sum_{i=1}^{\infty}\limits\lfloor \frac{n}{p^i}\rfloor\) ,这等于将 \(n\) 写成 \(p\) 进制数之后的所有前缀之和。

而 \(v_p({n\choose k})=v_p(n!)-v_p(k!)-v_p((n-k)!)\) ,因此 \(v_p({n\choose k})\) 就等于 \(k\) 与 \(n-k\) 在 \(p\) 进制意义下相加时的进位的次数。

数位 \(dp\) 即可。

点击查看代码
#include <bits/stdc++.h>
using namespace std;
const int md=1e9+7;
int p,alpha,a[2005],len;
char A[2005];
struct Big_based_p{
int len,x[4005];
inline void init(){
while(::len){
long long t=0;
for(int i=::len;i>=1;i--){
t=t*10+a[i],a[i]=t/p,t%=p;
if(!a[i]&&i==::len)--::len;
}
x[++len]=t;
}
}
int f[3503][3503][2][2];
inline void work(){
f[len+1][0][0][1]=1;
for(int i=len;i>=1;i--){
int c1=1ll*(p+1)*p/2%md,c2=1ll*(x[i]+1)*x[i]/2%md,c3=1ll*p*(p-1)/2%md;
int c4=1ll*x[i]*((p<<1)-x[i]-1)/2%md,c5=1ll*x[i]*(x[i]-1)/2%md,c6=1ll*x[i]*((p<<1)-x[i]+1)/2%md;
for(int j=0;j<=len-i+1;j++){
int f0=f[i+1][j][0][0],f1=f[i+1][j][0][1];
int f2=f[i+1][j][1][0],f3=f[i+1][j][1][1];
if(!f0&&!f1&&!f2&&!f3)continue;
f[i][j][0][0]=(1ll*c1*f0%md+1ll*c2*f1%md+1ll*c3*f2%md+1ll*c4*f3%md)%md;
f[i][j][0][1]=(1ll*(x[i]+1)*f1%md+1ll*(p-x[i]-1)*f3%md)%md;
f[i][j+1][1][0]=(1ll*c3*f0%md+1ll*c5*f1%md+1ll*c1*f2%md+1ll*c6*f3%md)%md;
f[i][j+1][1][1]=(1ll*x[i]*f1%md+1ll*(p-x[i])*f3%md)%md;
}
}
int res=0;
for(int i=alpha;i<=len;i++)res=(res+(f[1][i][0][0]+f[1][i][0][1])%md)%md;
printf("%d\n",res);
}
}lim;
int main(){
scanf("%d%d%s",&p,&alpha,A+1);len=strlen(A+1);
if(alpha>4000)return puts("0"),0;
for(int i=1;i<=len;i++)a[i]=A[len-i+1]-'0';
lim.init();lim.work(); return 0;
}

[AGC034E] Complete Compress

考虑枚举最终聚集的点,即枚举根节点,将不同子树中的棋子的深度两两抵消,判断是否能将所有棋子移到根。

点击查看代码
#include<bits/stdc++.h>
using namespace std;
int n;
int a[2005];
int ver[4005],ne[4005],head[2005],cnt;
inline void link(int x,int y){
ver[++cnt]=y;
ne[cnt]=head[x];
head[x]=cnt;
}
int siz[2005],dis[2005],f[2005];
void dfs(int x,int fi){
siz[x]=a[x];int son=0;
for(int i=head[x];i;i=ne[i]){
int u=ver[i];
if(u==fi)continue;
dfs(u,x);siz[x]+=siz[u];
dis[x]+=dis[u]+siz[u];dis[u]=dis[u]+siz[u];
if(dis[u]>dis[son])son=u;
}
if(!son)f[x]=0;
else if(2*dis[son]<=dis[x])f[x]=dis[x]/2;
else f[x]=dis[x]-dis[son]+min(f[son],(2*dis[son]-dis[x])/2);
}
int main(){
scanf("%d",&n);
for(int i=1;i<=n;i++)scanf("%1d",&a[i]);
for(int i=1;i<n;i++){
int x,y;scanf("%d%d",&x,&y);
link(x,y);link(y,x);
}
int res=1e9;
for(int i=1;i<=n;i++){
for(int j=1;j<=n;j++)siz[j]=0;
for(int j=1;j<=n;j++)dis[j]=0;
for(int j=1;j<=n;j++)f[j]=0;
dfs(i,i);
if(dis[i]&1)continue;
if(2*f[i]>=dis[i])res=min(res,dis[i]/2);
}
if(res==1e9)res=-1;
printf("%d\n",res); return 0;
}

[AGC020E] Encoding Subsets

枚举最后一段是否压缩,如何压缩,状态数不高于平方级别,可以记忆化搜索。

点击查看代码
#include<bits/stdc++.h>
using namespace std;
int n;
const int md=998244353;
map<__int128,int> dp[105];
int dfs(__int128 x,int len){
if(dp[len].count(x))return dp[len][x];
int res=dfs(x>>1,len-1)*(x&1?2:1)%md;
int len0=1;
for(__int128 lim=2;len0<=len;lim<<=1,len0++){
__int128 tmp=x%lim;
int len1=len0;
for(__int128 j=lim;len1+len0<=len;j*=lim,len1+=len0){
tmp&=(x/j);
res=(res+1ll*dfs(x/j/lim,len-len0-len1)*dfs(tmp,len0))%md;
}
}
return dp[len][x]=res;
}
char s[105];
int main(){
scanf("%s",s);
n=strlen(s);__int128 tmp=0;dp[0][0]=dp[1][0]=1;dp[1][1]=2;
for(int i=0;i<n;i++)tmp=tmp*2+s[i]-'0';
printf("%d\n",dfs(tmp,n)); return 0;
}

[AGC036D] Negative Cycle

图中不存在负环等价于差分约束系统有解,由于初始的 \(n-1\) 条边无法删去,因此最终的权值一定是单调不升的,差分后变为一个长度为 \(n-1\) 的 \(01\) 序列。

对于 \(i\to j\ (i<j)\) 的边,当 \(i,j\) 之间没有 \(1\) 时需要被删去,\(i\to j\ (i>j)\) 的边,在 \(i,j\) 之间有至少两个 \(1\) 时需要被删去,\(dp\) 即可。

点击查看代码
#include<bits/stdc++.h>
using namespace std;
int n;
long long A[505][505],B[505][505],f[505][505],ans=1e18;
inline long long val(int k,int j,int i){
return B[j+1][i]+A[n][j]-A[n][k]-A[i][j]+A[i][k];
}
int main(){
scanf("%d",&n);
for(int i=1;i<=n;i++){
for(int j=1;j<=n;j++){
if(i==j)continue;
scanf("%lld",&A[i][j]);
}
}
for(int j=1;j<=n;j++){
for(int i=j;i;i--)B[i][j]=B[i+1][j]+B[i][j-1]-B[i+1][j-1]+A[i][j];
}
for(int i=1;i<=n;i++){
for(int j=1;j<=n;j++)A[i][j]=A[i][j]+A[i-1][j]+A[i][j-1]-A[i-1][j-1];
}
for(int i=1;i<n;i++){
f[i][0]=val(0,0,i);ans=min(ans,f[i][0]+val(0,i,n));
for(int j=1;j<i;j++){
f[i][j]=1e18;
for(int k=0;k<j;k++)f[i][j]=min(f[i][j],f[j][k]+val(k,j,i));
ans=min(ans,f[i][j]+val(j,i,n));
}
}
printf("%lld\n",ans); return 0;
}

[AGC028D] Chords

发现这个圆上的问题其实可以直接铺平到序列上,考虑在每个联通块的最左和最右端点处统计该连通块。

预处理 \(g[i]\) 表示 \(i\) 给点两两之间任意匹配的方案数,对于每个区间,枚举最左边的小联通块,容斥掉不合法方案数即可。

点击查看代码
#include<bits/stdc++.h>
using namespace std;
int n,k;
const long long md=1e9+7;
int vis[605],tot[605][605];
unsigned long long hsh[605][605],tag[605];
inline unsigned long long rd(){
return 1ll*rand()*rand()*20050115+rand()*2005+rand();
}
long long dp[605][605],g[605],ans;
int main(){
scanf("%d%d",&n,&k);n<<=1;
g[0]=1;
for(int i=2;i<=n;i+=2)g[i]=g[i-2]*(i-1)%md;
for(int i=1;i<=k;i++){
int x,y;scanf("%d%d",&x,&y);
unsigned long long tmp=rd();
tag[x]^=tmp;tag[y]^=tmp;vis[x]=-1;vis[y]=-1;
}
for(int i=1;i<=n;i++){
for(int j=i;j<=n;j++){
hsh[i][j]=hsh[i][j-1]^tag[j];
tot[i][j]=tot[i][j-1]+1+vis[j];
}
}
for(int len=1;len<=n;len++){
for(int l=1;l+len<=n;l++){
int r=l+len;
if(hsh[l][r])continue;
dp[l][r]=g[tot[l][r]];
for(int t=l;t<r;t++)dp[l][r]=(dp[l][r]-dp[l][t]*g[tot[t+1][r]])%md;
ans=(ans+dp[l][r]*g[tot[1][n]-tot[l][r]])%md;
}
}
printf("%lld",(ans+md)%md); return 0;
}

[AGC022F] Checkers

首先考虑连边,建出一棵树,把每次消掉的 \(B\) 的父亲设成 \(A\) ,那么就会发现每一个点的贡献就是 \(a_ic_ix_i\) 其中 \(a_i=2^d\),\(d\) 为深度,\(c_i\) 是 \(1\) 或 \(-1\)。

因为 \(x_i\) 很大,所以任意两个点之间的贡献可以看做不会互相抵消,那么也就是说对于两个方案只要存在一个 \(i\) 使得 \(c_i\) 或者 \(a_i\) 不同,那么就算做不同的方案。

然后发现 \(a_i\) 只跟深度有关,那么这里先讨论 \(c_i\),我们可以默认根节点的 \(c_i=1\),那么发现对于一个点,它的 \(c_i\) 取决于它儿子个数的奇偶性和父亲选这个儿子的时候,之前选了儿子个数的奇偶性。

然后我们考虑根据这个性质进行dp,设 \(f_{i,j}\) 表示选了 \(i\) 个点,有 \(j\) 个点的儿子个数是奇数

那么每次就可以枚举一个当前层数有 \(k\) 个节点,然后我们发现是偶数的儿子个数为 \(\frac {k-j}{2}\) (如果发现 \(k-j\) 的奇偶性不对那就不转移),也就是说如果不计算下一层的影响的话,将会有 \(\frac {k-j}{2}\) 个点与父亲的符号相同。

考虑计算下一层的影响,枚举一个 \(x\) 表示实际上有 \(x\) 个节点和父亲不同,那么至少需要 \(|\frac {k-j}{2}-x|\) 个奇数点。

点击查看代码
#include<bits/stdc++.h>
using namespace std;
const int md=1e9+7;
int n,dp[55][55];
long long fac[55],inv[55];
inline void init(){
fac[0]=fac[1]=inv[0]=inv[1]=1;
for(int i=2;i<=n;i++)fac[i]=fac[i-1]*i%md;
for(int i=2;i<=n;i++)inv[i]=(md-md/i)*inv[md%i]%md;
for(int i=2;i<=n;i++)inv[i]=inv[i]*inv[i-1]%md;
}
long long C(int n,int m){
return fac[n]*inv[m]%md*inv[n-m]%md;
}
int main(){
scanf("%d",&n);init();
dp[1][1]=dp[1][0]=n;
for(int i=1;i<n;i++){
for(int j=1;j<=n-i;j++){
for(int k=0;k<=j;k++){
if((j-k)&1)continue;
int x=(j-k)/2;
for(int d=0;d<=j;d++){
dp[i+j][abs(x-d)]=(dp[i+j][abs(x-d)]+dp[i][k]*C(n-i,j)%md*C(j,d))%md;
}
}
}
}
printf("%d\n",dp[n][0]);
return 0;
}

CF379G New Year Cactus

仿照树上背包的形式,在仙人掌上做背包即可。

点击查看代码
#include<bits/stdc++.h>
using namespace std;
int n,m;
int dp[3][2505][2505],tmp[2505],siz[2505],tot[2][3][2505],ANS[3][2505];
inline void calc(int x,vector<int> vec){
if(vec.size()==1){
for(int i=0;i<=siz[x]+siz[vec[0]];i++)tmp[i]=-1e9;
for(int i=0;i<=siz[x];i++){
for(int j=0;j<=siz[vec[0]];j++)tmp[i+j]=max(tmp[i+j],dp[0][x][i]+dp[0][vec[0]][j]);
for(int j=0;j<=siz[vec[0]];j++)tmp[i+j]=max(tmp[i+j],dp[0][x][i]+dp[1][vec[0]][j]);
for(int j=0;j<=siz[vec[0]];j++)tmp[i+j]=max(tmp[i+j],dp[0][x][i]+dp[2][vec[0]][j]);
}
for(int i=0;i<=siz[x]+siz[vec[0]];i++)dp[0][x][i]=tmp[i];
for(int i=0;i<=siz[x]+siz[vec[0]];i++)tmp[i]=-1e9;
for(int i=0;i<=siz[x];i++){
for(int j=0;j<=siz[vec[0]];j++)tmp[i+j]=max(tmp[i+j],dp[1][x][i]+dp[0][vec[0]][j]);
for(int j=0;j<=siz[vec[0]];j++)tmp[i+j]=max(tmp[i+j],dp[1][x][i]+dp[1][vec[0]][j]);
}
for(int i=0;i<=siz[x]+siz[vec[0]];i++)dp[1][x][i]=tmp[i];
for(int i=0;i<=siz[x]+siz[vec[0]];i++)tmp[i]=-1e9;
for(int i=0;i<=siz[x];i++){
for(int j=0;j<=siz[vec[0]];j++)tmp[i+j]=max(tmp[i+j],dp[2][x][i]+dp[0][vec[0]][j]);
for(int j=0;j<=siz[vec[0]];j++)tmp[i+j]=max(tmp[i+j],dp[2][x][i]+dp[2][vec[0]][j]);
}
for(int i=0;i<=siz[x]+siz[vec[0]];i++)dp[2][x][i]=tmp[i];siz[x]+=siz[vec[0]];
}
else {
int u=vec.back();vec.pop_back();
reverse(vec.begin(),vec.end());vec.push_back(x);
{
int sum=siz[u];
for(auto it:vec)sum+=siz[it];
for(int i=0;i<=sum;i++)ANS[0][i]=-1e9;
for(int i=0;i<=sum;i++)ANS[1][i]=-1e9;
for(int i=0;i<=sum;i++)ANS[2][i]=-1e9;
}
for(int t=0;t<3;t++){
int *f[3]={tot[0][0],tot[0][1],tot[0][2]},*g[3]={tot[1][0],tot[1][1],tot[1][2]};
int sum=siz[u];for(int i=0;i<=sum;i++)f[t][i]=dp[t][u][i];
for(int i=0;i<=sum;i++)f[(t+1)%3][i]=-1e9;
for(int i=0;i<=sum;i++)f[(t+2)%3][i]=-1e9;
for(auto it:vec){
for(int i=0;i<=sum+siz[it];i++)tmp[i]=-1e9;
for(int i=0;i<=sum;i++){
for(int j=0;j<=siz[it];j++)tmp[i+j]=max(tmp[i+j],f[0][i]+dp[0][it][j]);
for(int j=0;j<=siz[it];j++)tmp[i+j]=max(tmp[i+j],f[1][i]+dp[0][it][j]);
for(int j=0;j<=siz[it];j++)tmp[i+j]=max(tmp[i+j],f[2][i]+dp[0][it][j]);
}
for(int i=0;i<=sum+siz[it];i++)g[0][i]=tmp[i];
for(int i=0;i<=sum+siz[it];i++)tmp[i]=-1e9;
for(int i=0;i<=sum;i++){
for(int j=0;j<=siz[it];j++)tmp[i+j]=max(tmp[i+j],f[0][i]+dp[1][it][j]);
for(int j=0;j<=siz[it];j++)tmp[i+j]=max(tmp[i+j],f[1][i]+dp[1][it][j]);
}
for(int i=0;i<=sum+siz[it];i++)g[1][i]=tmp[i];
for(int i=0;i<=sum+siz[it];i++)tmp[i]=-1e9;
for(int i=0;i<=sum;i++){
for(int j=0;j<=siz[it];j++)tmp[i+j]=max(tmp[i+j],f[0][i]+dp[2][it][j]);
for(int j=0;j<=siz[it];j++)tmp[i+j]=max(tmp[i+j],f[2][i]+dp[2][it][j]);
}
for(int i=0;i<=sum+siz[it];i++)g[2][i]=tmp[i];sum+=siz[it];
swap(f[0],g[0]);swap(f[1],g[1]);swap(f[2],g[2]);
}
for(int i=0;i<=sum;i++)ANS[0][i]=max(ANS[0][i],f[0][i]);
if(t!=2)for(int i=0;i<=sum;i++)ANS[1][i]=max(ANS[1][i],f[1][i]);
if(t!=1)for(int i=0;i<=sum;i++)ANS[2][i]=max(ANS[2][i],f[2][i]);
}
int sum=siz[u];for(auto it:vec)sum+=siz[it];siz[x]=sum;
for(int i=0;i<=sum;i++)dp[0][x][i]=ANS[0][i];
for(int i=0;i<=sum;i++)dp[1][x][i]=ANS[1][i];
for(int i=0;i<=sum;i++)dp[2][x][i]=ANS[2][i];
}
}
vector<int> vec[2505];
int dfn[2505],low[2505],cnt;
int stk[2505],top;
void tarjan(int x){
siz[x]=1;dp[0][x][1]=-1e9;
dp[1][x][0]=1;dp[1][x][1]=-1e9;dp[2][x][0]=-1e9;
dfn[x]=low[x]=++cnt;stk[++top]=x;
for(auto u:vec[x]){
if(!dfn[u]){
tarjan(u);low[x]=min(low[x],low[u]);
if(low[u]>=dfn[x]){
vector<int> tmp;
while(stk[top]!=u)tmp.push_back(stk[top--]);
tmp.push_back(stk[top--]);calc(x,tmp);
}
}
else low[x]=min(low[x],dfn[u]);
}
}
int main(){
scanf("%d%d",&n,&m);
for(int i=1;i<=m;i++){
int x,y;scanf("%d%d",&x,&y);
vec[x].push_back(y);vec[y].push_back(x);
}
tarjan(1);
for(int i=0;i<=n;i++)printf("%d ",max(dp[0][1][i],max(dp[1][1][i],dp[2][1][i]))); return 0;
}

[USACO22FEB] Phone Numbers P

考虑将后三位的值以及某一位是否可以作为转移点记录在状态中,复杂度比较高,发现很多状态是没用的,写一个结构体,用 \(\text{map}\) 记录状态即可。

点击查看代码
#include<bits/stdc++.h>
using namespace std;
int T;
int n,cnt[1<<10];
char s[100005];
const int md=1e9+7;
bool vis2[1<<10],vis4[1<<10];
inline void init(){
vis2[(1<<1)+(1<<2)]=vis2[(1<<2)+(1<<3)]=vis2[(1<<4)+(1<<5)]=vis2[(1<<5)+(1<<6)]=vis2[(1<<7)+(1<<8)]=vis2[(1<<8)+(1<<9)]=1;
vis2[(1<<1)+(1<<4)]=vis2[(1<<4)+(1<<7)]=vis2[(1<<2)+(1<<5)]=vis2[(1<<5)+(1<<8)]=vis2[(1<<3)+(1<<6)]=vis2[(1<<6)+(1<<9)]=1;
vis4[(1<<1)+(1<<2)+(1<<4)+(1<<5)]=vis4[(1<<2)+(1<<3)+(1<<5)+(1<<6)]=1;
vis4[(1<<4)+(1<<5)+(1<<7)+(1<<8)]=vis4[(1<<5)+(1<<6)+(1<<8)+(1<<9)]=1;
for(int i=0;i<(1<<10);i++)cnt[i]=cnt[i>>1]+(i&1);
}
inline int Get2(int i){
return (1<<s[i])|(1<<s[i-1]);
}
inline int Get4(int i){
return (1<<s[i])|(1<<s[i-1])|(1<<s[i-2])|(1<<s[i-3]);
}
struct node{
int a0,a1,a2,a3;
node(int _a0,int _a1,int _a2,int _a3){
a0=_a0;a1=_a1;a2=_a2;a3=_a3;
}
inline bool operator <(const node &b)const{
if(a0!=b.a0)return a0<b.a0;
if(a1!=b.a1)return a1<b.a1;
if(a2!=b.a2)return a2<b.a2;
return a3<b.a3;
}
};
inline bool check(int s,int N,int S){
if(cnt[s]!=N)return 0;
if((s&S)!=s)return 0;
if(!vis4[S])return 0;
return 1;
}
map<node,int> f,g;
inline void solve(){
scanf("%s",s+1);n=strlen(s+1);
for(int i=1;i<=n;i++)s[i]-='0';
f.clear();g.clear();f[node(-1,-1,-1,0)]=1;
for(int i=1;i<=n;i++){
for(auto it:f){
node pre=it.first;
for(int t=1;t<=9;t++){
node T(pre.a1|(1<<t),pre.a2|(1<<t),pre.a3|(1<<t),-1);
if(pre.a0!=-1&&(pre.a0|(1<<t))==Get4(i)&&vis4[Get4(i)])T.a3=0;
if(T.a1!=-1&&T.a1==Get2(i)&&vis2[Get2(i)])T.a3=0;
if(T.a2!=-1&&T.a2==(1<<s[i]))T.a3=0;
if(T.a0!=-1&&(i==n||!check(T.a0,3,Get4(i+1))))T.a0=-1;
if(T.a1!=-1&&(i>=n-1||!check(T.a1,2,Get4(i+2))))T.a1=-1;
if((~T.a0)||(~T.a1)||(~T.a2)||(~T.a3))g[T]=(g[T]+it.second)%md;
}
}
swap(g,f);g.clear();
}
int ans=0;
for(auto it:f)if(!it.first.a3)ans=(ans+it.second)%md;
printf("%d\n",ans);
}
int main(){
init();
scanf("%d",&T);
while(T--)solve(); return 0;
}

[ARC068D] Solitaire

首先发现那个双端队列中的数一定先单调递减,然后再单调递增,最小值为 \(\text{1}\)。

现在考虑从双端队列中取数,那么当我们取到 \(\text{1}\) 这个数时,我们会在原来的双端队列中取到类似这样的两个数列:(分别用红、蓝表示)

那么红、蓝两数列的总长度为 \(k\),剩下的就是绿色的一段,长度为 \(n-k\),我们每次可以从两端任意取,共取 \(n-k-1\) 次,方案数为 \(2^{n-k-1}\) 。

所以我们只用考虑前 \(k\) 个数的取法,然后乘上 \(2^{n-k-1}\) 就好了。

由图像,我们看一下弹出序列需要满足什么条件:

首先第 \(k\) 位肯定是 \(\text{1}\)。(题目要求)

前 \(k\) 个数,一定是由一个或两个单调递减的数列混合而成的(这两个数列就是图中的红、蓝两个数列),并且其中的一个单调递减的数列肯定包含 \(\text{1}\) 这个点(蓝色数列)。

后 \(n-k\) 个数,其最大值应小于某一个提到的单调数列的最小值。也就是绿色段的最大值小于红色段的最小值。

那我们不妨设 \(f(i,j)\) 表示已经取了前 \(i\) 个数,他们的最小值为 \(j\) 时的方案数。(满足上面的 \(\text{3}\) 个条件)

那么我们设选的这 \(i\) 个数分成的两个单调递减的序列分别为 \(A\)、\(B\),其中最小值 \(j\) 在 \(A\) 中,\(B\) 中的最小值为 \(\text{minn}\)。

设除了我们选的这 \(i\) 个数剩下的 \(n-i\) 个数组成的集合为 \(S\),\(S\) 中最大的数为 \(\text{maxS}\)。

由 \(f(i,j)\) 的定义得,\(B\) 序列已经满足了第 \(\text{3}\) 个条件,即 \(\text{maxS}<\text{minn}\)。

我们现在要从 \(S\) 中选一个数,加入 \(A\) 或 \(B\) 里面。

考虑构造:

假设加入的数 \(x\) 满足 \(x<j\),那么我们就把它加到 \(A\) 的末尾,此时仍满足所有条件。所以 \(f(i,j)\) 向 \(f(i+1,1)\sim f(i+1,j-1)\) 转移。

假设加入的数 \(x\) 满足 \(x\geq j\),如果加入 \(A\) 中,\(A\) 就不满足单调递减的性质了,所以只能加入 \(B\) 中。

然后发现只能把 \(S\) 中的最大值 \(\text{maxS}\) 加入到 \(B\) 的队尾(而且必须得满足 \(\text{maxS}\geq j\),不然会在第一种情况中算重)。

由于 \(\text{maxS}\) 是 \(S\) 中的最大的数,所以当 \(\text{maxS}\) 加入 \(B\) 数列成为 \(B\) 数列的最小值后,仍大于 \(S\) 中的所有数,满足条件。

所以 \(f(i,j)\) 向 \(f(i+1,j)\) 转移。

至于为什么 \(S\) 中除 \(\text{maxS}\) 的某一个数 \(x\) 都不能加入 \(B\) 的队尾:因为加入后,\(B\) 中的最小值就变成了 \(x\),而 \(S\) 中的最大值仍为 \(\text{maxS}\)。此时 \(x<\text{maxS}\),不满足条件。

所以通过 \(f(i,j)\) 就可以向 \(f(i+1,1\sim j)\) 转移了。

显然答案就是 \(2^{n-k-1}\times\sum\limits_{i=2}^{n-k+2}f(k-1,i)\) 。

发现这等价于一个格路计数问题,可以用类似卡特兰数的方式做到线性。

点击查看代码
#include<bits/stdc++.h>
using namespace std;
int n,k;
const int md=1e9+7;
int fac[4005],inv[4005];
inline void init(){
fac[0]=fac[1]=inv[0]=inv[1]=1;
for(int i=2;i<=n+k;i++)fac[i]=1ll*fac[i-1]*i%md;
for(int i=2;i<=n+k;i++)inv[i]=1ll*(md-md/i)*inv[md%i]%md;
for(int i=2;i<=n+k;i++)inv[i]=1ll*inv[i]*inv[i-1]%md;
}
inline long long C(int x,int y){
if(!y)return 1;
if(x<0||y<0||x<y)return 0;
return 1ll*fac[x]*inv[y]%md*inv[x-y]%md;
}
inline long long pwr(int x,int y){
int res=1;
while(y>0){
if(y&1)res=1ll*res*x%md;
x=1ll*x*x%md;y>>=1;
}
return res;
}
int main(){
scanf("%d%d",&n,&k);init();
int tmp=1ll*((C(n+k-3,n-2)-C(n+k-3,n))%md+md)%md*pwr(2,n-k-1)%md;
printf("%d\n",n==1&&k==1?1:tmp); return 0;
}

[ARC056B] Range Argmax

[ABC221H] Count Multiset

[AGC004E] Salvage Robots

[Cnoi2019]青染之心

要有光

CF848D Shake It!

CF1667E Centroid Probabilities

多校联训 DP 专题的更多相关文章

  1. 多校联训 DS 专题

    CF1039D You Are Given a Tree 容易发现,当 \(k\) 不断增大时,答案不断减小,且 \(k\) 的答案不超过 \(\lfloor\frac {n}{k}\rfloor\) ...

  2. 决策单调性优化dp 专题练习

    决策单调性优化dp 专题练习 优化方法总结 一.斜率优化 对于形如 \(dp[i]=dp[j]+(i-j)*(i-j)\)类型的转移方程,维护一个上凸包或者下凸包,找到切点快速求解 技法: 1.单调队 ...

  3. 状压dp专题复习

    状压dp专题复习 (有些题过于水,我直接跳了) 技巧总结 : 1.矩阵状压上一行的选择情况 \(n * 2^n\) D [BZOJ2734][HNOI2012]集合选数 蒻得不行的我觉得这是一道比较难 ...

  4. 树形dp专题总结

    树形dp专题总结 大力dp的练习与晋升 原题均可以在网址上找到 技巧总结 1.换根大法 2.状态定义应只考虑考虑影响的关系 3.数据结构与dp的合理结合(T11) 4.抽直径解决求最长链的许多类问题( ...

  5. 区间dp专题练习

    区间dp专题练习 题意 1.Equal Sum Partitions ? 这嘛东西,\(n^2\)自己写去 \[\ \] \[\ \] 2.You Are the One 感觉自己智力被吊打 \(dp ...

  6. DP专题训练之HDU 2955 Robberies

    打算专题训练下DP,做一道帖一道吧~~现在的代码风格完全变了~~大概是懒了.所以.将就着看吧~哈哈 Description The aspiring Roy the Robber has seen a ...

  7. DP专题:划分数问题

    一.这个专题有什么用 练练DP 练练组合数学 ...... 二.正题 此类问题有如下几种形态: 1. 将n划分成若干正整数之和的划分数.2. 将n划分成k个正整数之和的划分数.3. 将n划分成最大数不 ...

  8. 【dp专题】NOIP真题-DP专题练习

    这里学习一下DP的正确姿势. 也为了ZJOI2019去水一下做一些准备 题解就随便写写啦. 后续还是会有专题练习和综合练习的. P1005 矩阵取数游戏 给出$n \times m$矩阵每次在每一行取 ...

  9. dp专题训练

    ****************************************************************************************** 动态规划 专题训练 ...

随机推荐

  1. python相关知识理解

    Python3 基础了解 编码  Python 3 源码文件以 UTF-8 编码,所有字符串都是 unicode 字符串   # -*- coding: cp-1252 -*- 标识符 · 第一个字符 ...

  2. KotlinMall实战之注册部分MVP架构配置

    包目录如下: ①BaseView部分:基本的回调 interface BaseView { fun showLoading() fun hideLoading() fun onError()} ②Ba ...

  3. XCTF练习题---MISC---wireshark1

    XCTF练习题---MISC---wireshark1 flag:flag{ffb7567a1d4f4abdffdb54e022f8facd} 解题步骤: 1.观察题目,下载附件 2.得到一个wire ...

  4. 论文解读(SUBG-CON)《Sub-graph Contrast for Scalable Self-Supervised Graph Representation Learning》

    论文信息 论文标题:Sub-graph Contrast for Scalable Self-Supervised Graph Representation Learning论文作者:Yizhu Ji ...

  5. Spring (IOC)配置

    就是这个东西,里面的不同标签,所代表的不同含义 beans  里面有很多的bean  ,每一个bean都是容器里面的一个对象 1.别名alias  (另外的一个名字) XML <alias na ...

  6. Windows下查找各类游戏存档路径

    我算是个比较爱打单机游戏的人,同时也是个半吊子的编程爱好者,有的时候会去干一些修改存档的事儿.不过这篇博文不讲存档修改技术,只讲第一步:去哪找存档? 目标:在windows10系统下搜索到游戏的存档路 ...

  7. B+树能存多少数据?

    B+树能存多少数据? 图 MySQL B+树示意图 InnoDB页的大小默认是16KB: 假设一条记录大小为1KB,则一个数据页中可以存16条数据(忽略页中的其他数据结构) 假设主键为int,又指针大 ...

  8. 这些 Shell 分析服务器日志命令集锦,收藏好

    关注「开源Linux」,选择"设为星标" 回复「学习」,有我为您特别筛选的学习资料~ 自己的小网站跑在阿里云的ECS上面,偶尔也去分析分析自己网站服务器日志,看看网站的访问量.看看 ...

  9. 使用 .net + blazor 做一个 kubernetes 开源文件系统

    背景 据我所知,目前 kubernetes 本身或者其它第三方社区都没提供 kubernetes 的文件系统.也就是说要从 kubernetes 的容器中下载或上传文件,需要先进入容器查看目录结构,然 ...

  10. 关于 MyBatis-Plus 分页查询的探讨 → count 都为 0 了,为什么还要查询记录?

    开心一刻 记得上初中,中午午休的时候,我和哥们躲在厕所里吸烟 听见外面有人进来,哥们猛吸一口,就把烟甩了 进来的是教导主任,问:你们干嘛呢? 哥们鼻孔里一边冒着白烟一边说:我在生气 环境搭建 依赖引入 ...