专题-DP
前言:
激动的心,颤抖的手,菜鸡又来水博客惹~~ 一天做了贺了四道题,真充实啊!!┭┮﹏┭┮
\(T1:\)CF1970G2 Min-Fund Prison (Medium)
思路:
题目要求\(x^2+y^2+k×c\)的最小值:首先c是一定的,所以不用考虑。对于\(k\)来说,我们知道每连一条割边就会减少一个连通块,所以\(k\)的值为连通块的数量\(-2\)。然后不固定的就只剩\(x,y\)了,但\(x,y\)的和也是定值,所以我们就可以开心的枚举啦~~
首先建图肯定是不用多说的。然后看眼数据范围,嗯,比较友好。那我们就枚举每一条连接\(x,y\)的边,那么此时,这条边判为两种情况:要么这条边是本来就存在在图上的;要么是我们后来给它加上的。对于原本就存在的边,我们只需要给它直接删除就好;对于原本没有的边,我们要先给这条边加上,然后再给它删除就好啦~~
\(ps:\)打码中的\(f[i]\)表示把图分为大小为\(i\)和\(n-i\)的两个连通块是符合情况的。
代码:
#include<iostream>
#include<bitset>
#include<vector>
#include<cstring>
#define int long long
using namespace std;
const int N=305;
int T,n,m,c,x,y,num,tot,ans=1e18;bool a[N][N],vis[N];bitset<N> f;vector<int> siz,v[N];
inline void init(){
ans=1e18;
memset(a,0,sizeof(a));
for(int i=1;i<=n;i++) v[i].clear();
}//多组数据清空
inline void dfs(int x){
if(vis[x]) return ;
vis[x]=1;
num++;
for(int now:v[x]) if(a[x][now]) dfs(now);
}//搜索连通块的大小
signed main(){
ios::sync_with_stdio(false);
cin>>T;
while(T--){
cin>>n>>m>>c;
init();
for(int i=1;i<=m;i++){
cin>>x>>y;
v[x].push_back(y);v[y].push_back(x);//建边(不建议用链式前向星,后面处理起来不太方便【也可能是我菜,那就当我没说】)
a[x][y]=a[y][x]=1;//标记边存在
}
for(int x=1;x<n;x++){
for(int y=x+1;y<=n;y++){//遍历x,y
int k=a[x][y]^1;//记录本条边是否存在方便后面回溯
bool tmp=a[x][y];tot=0;
if(k==1){//不存在
v[x].push_back(y);v[y].push_back(x);
a[x][y]=a[y][x]=1;
}//加边
memset(vis,0,sizeof(vis));//清空
for(int i=1;i<=n;i++) if(!vis[i]) tot++,dfs(i);//tot统计连通块的数量
if(k==1){
v[x].pop_back();v[y].pop_back();
a[x][y]=a[y][x]=0;
}//再把边删除
a[x][y]=a[y][x]=0;
siz.clear();
memset(vis,0,sizeof(vis));
for(int i=1;i<=n;i++) if(!vis[i]) num=0,dfs(i),siz.push_back(num);//储存连通块的大小
if(siz.size()>tot){//删边后连通块数量增多,即删除的是割边
k+=siz.size()-2;
f.reset();//清空
f[0]=1;//初始化
int l=siz.size();
for(int i=1;i<=l;i++) f|=(f<<siz[i]);//可能出现的情况
for(int i=0;i<=n;i++) if(f[i]) ans=min(ans,i*i+(n-i)*(n-i)+k*c);//统计答案
}
a[x][y]=a[y][x]=tmp;//回溯
}
}
if(ans==1e18) cout<<-1<<'\n';//判无解
else cout<<ans<<'\n';//输出答案~~
}
return 0;
}
\(T2:\) CF1917F Construct Tree
思路:做了 晚上再敲思路 先粘没有注释的代码
代码~~:
#include<iostream>
#include<bitset>
using namespace std;
const int N=2050;
int T,n,l[N],d,maxn,maxx;bool g,h;
bitset<2050> f[2050],o;
int main(){
ios::sync_with_stdio(false);
cin>>T;
while(T--){
maxn=maxx=0;
cin>>n>>d;
for(int i=1;i<=n;i++) cin>>l[i],maxn=max(l[i],maxn);
for(int i=0;i<=d;i++){
o[i]=0;
for(int j=0;j<=d-i;j++) f[i][j]=0;
}f[0][0]=1;o[0]=1;h=0;
for(int i=1;i<=n;i++){
for(int j=d;j>=0;j--){
f[j]|=(f[j]<<l[i]);
if(j>=l[i]) f[j]|=f[j-l[i]];
}
if(h==1||l[i]!=maxn) o|=(o<<l[i]),maxx=max(maxx,l[i]);
else h=1;
}
g=o[d-maxn];
for(int i=0;i<=d;i++) if(i>=maxn&&d-i>=maxn&&f[i][d-i]==1) g=1;
if(g==1&&maxn+maxx<=d) cout<<"Yes"<<'\n';
else cout<<"No"<<'\n';
}
return 0;
}
\(T3:\)CF1914G2 Light Bulbs (Hard Version)
思路:
先奉上一位讲的超清晰的大佬
然后开始我的讲解啦:这个题目分为两个子任务。
任务一:
由题意及样例可知,如果一个大区间包含一个小区间,那么小区间不必被点亮。如果两个区间相互交叉,则点亮其中任意一个即可。所以,题目所求不过是不相交区间的个数罢了。
任务二:
任务二的烧烤量就比较大了:让求集合\(S\)的数量。
有任务一我们可以分析出,若干相交的区间只要有一个灯被点亮那其余的的灯就都都会被点亮,这些区间是对答案有贡献的区间。而被包含的的区间则是对答案没有贡献的区间。所以我们可以遍历每一个长区间(有包含关系的区间和相互交叉的区间),然后跳过其中被包含的闭合小区间,算出其他长区间内数字的个数,在将几个长区间内的答案相乘,就求出最终答案了!!!
代码:
#include<iostream>
#include<map>
#include<random>
#define int long long
using namespace std;
const int N=4e5+5,mod=998244353;
int T,n,c,ans1,ans2,cur[N],w[N];map<int,int> last;
mt19937_64 rnd(random_device{}());
inline int get(){
int x=0;
while(!x) x=rnd();
return x;
}//随机数
signed main(){
ios::sync_with_stdio(false);
cin>>T;
while(T--){
cin>>n;
for(int i=1;i<=2*n;i++) w[i]=get();//随机化哈希
last.clear();
ans1=0;ans2=1;//多组数据
for(int i=1;i<=2*n;i++){
cin>>c;
cur[i]=cur[i-1]^w[c];//异或哈希
last[cur[i]]=i;//记录该哈希值出现的最后一个位置,即跳过所有包含的闭合小区间后的位置
if(!cur[i]) ans1++;//不想交区间的个数
}
for(int i=0;i<2*n;i++){//遍历
if(cur[i]) continue;//从区间端点开始
int j=i+1,res=1;
while(cur[j]){//跳过中间闭合的小区间,即跳过被包含的区间
j=last[cur[j]]+1;
res++;//其余区间都有贡献
}
ans2=(ans2*res)%mod;//乘积
}
cout<<ans1<<" "<<ans2<<'\n';//输出答案~~
}
return 0;
}
\(T4:\) CF1906H Twin Friends
思路:
首先题目要求:如果至少有一个昵称不同,则认为两个昵称对不同。 所以我们需要把哥哥昵称的方案数与弟弟昵称的方案数相乘。哥哥昵称的方案数很好求,即为\(A\)的全排列数,即为\(n!\)。接下来我们需要考虑弟弟昵称的方案数。这里我们就需要使用\(dp\)了:首先有一维肯定表示一下我们遍历到哪了,又由题可知排列顺序待定,所以我们不妨按字母进行遍历。又因为题目说可以使用字母表的下一个字母,所以第二维我们设已经用了\(j\)个下一位字母。所以,\(dp[i][j]\)表示当前遍历到地\(i\)个字母,使用了\(j\)个下一个字母。易得递推方程\(f[i][j]=C_{a[i]}^j* \sum_{k=0}^{b[i]+j-a[i]}f[i-1][k].\)
所以,最后的答案为\(f[26][0]*\frac{n!}{Πa[i]!}\).
代码:
#include<iostream>
#define int long long
using namespace std;
const int N=200000+5,mod=998244353;
int m,n,ans,cnt1[30],cnt2[30],sum[N],f[30][N],power[N];string s,t;
inline int qpow(int x,int y){
int res=1;
while(y){
if(y&1) res=(res*x)%mod;
x=(x*x)%mod;
y>>=1;
}
return res;
}//快速幂
inline int inv(int x){
if(x) return qpow(x,mod-2);
else return 1;
}//求逆元
signed main(){
ios::sync_with_stdio(false);
cin>>n>>m>>s>>t;
for(int i=0;i<n;i++) cnt1[s[i]-'A'+1]++;//统计哥哥昵称中每个字符出现的次数
for(int i=0;i<m;i++) cnt2[t[i]-'A'+1]++;//统计弟弟昵称中每个字符出现的次数
power[0]=1;for(int i=1;i<=m;i++) power[i]=(power[i-1]*i)%mod;//预处理阶乘
f[0][0]=1;ans=power[n];
for(int i=1;i<=26;i++){
sum[0]=f[i-1][0];//前缀和
ans=(ans*inv(power[cnt1[i]]))%mod;//除以a[i]的阶乘
for(int j=1;j<=cnt2[i];j++) sum[j]=(sum[j-1]+f[i-1][j])%mod;//前缀和
for(int j=cnt1[i]-cnt2[i];j<=min(cnt1[i],cnt2[i+1]);j++)
f[i][j]=power[cnt1[i]]*inv(power[j])%mod*inv(power[cnt1[i]-j])%mod*sum[cnt2[i]+j-cnt1[i]]%mod;//套公式
}
ans=(ans*f[26][0])%mod;//求出最终答案
cout<<ans<<'\n';//完结撒花~~
return 0;
}
\(T5:\)CF1905E One-X
详情见另一篇博客
\(T6:\) CF1874C Jellyfish and EVA
思路:还没做。。
\(T7:\)CF1870E Another MEX Problem
思路:
我们设 \(f_{i,j}\) 表示前 \(i\) 个数选出若干子段后能否得到 \(j\).易得转移方程为\(f_{i,j}|=f_{k−1,j⊕mex(k,i)}.\)此时的时间复杂度是\(O(n^3)\)的,不能接受,因此我们考虑优化。易得结论有效区间的数目小于等于\(2n\)个(证明见下),那么我们只需要遍历有效区间即可,此时我们的时间复杂度为\(O(n^2)\)。便可以通过此题啦~~
证明:
简单的反证法可以证明,假设每个点作为区间两端的中较大的一个(避免重复计算),最多只有 \(2\)
个没法更小的区间(向左、向右各一个),共 \(2×n\) 个。
代码:
#include<iostream>
#include<vector>
using namespace std;
const int N=5050;
int T,n,ans,a[N],mex[N][N];bool f[N][N],flag[N];vector<int> l[N];
int main(){
ios::sync_with_stdio(false);
cin>>T;
while(T--){
cin>>n;ans=0;
for(int i=1;i<=n;i++) cin>>a[i],l[i].clear();
for(int i=1;i<=n;i++) for(int j=0;j<=5005;j++) f[i][j]=0;f[0][0]=1;//初始化
for(int i=1;i<=n;i++){
int mx=0;
for(int j=0;j<=5005;j++) flag[j]=0;
for(int j=i;j<=n;j++){
flag[a[j]]=1;
while(flag[mx]) mx++;
mex[i][j]=mx;
}
}//先暴力预处理出区间mex值
for(int i=1;i<=n;i++) for(int j=0;j<=5005;j++) if(mex[i][j]!=mex[i+1][j]&&mex[i][j]!=mex[i][j-1]) l[j].push_back(i);//好区间
for(int i=1;i<=n;i++){
for(int j=0;j<=5000;j++){
f[i][j]|=f[i-1][j];
for(int k:l[i]) if((j^mex[k][i])<=5000) f[i][j]|=f[k-1][j^mex[k][i]];
}
}
for(int i=5001;i>=0;i--){
if(f[n][i]){
ans=i;
break;
}//遍历答案
}
cout<<ans<<'\n';//完结撒花~~
}
return 0;
}
\(T8:\)CF1868C Travel Plan
思路:也没做。。。
\(T9:\)CF1866M Mighty Rock Tower
思路:
不会的题先推大佬题解
按理来说我们设的状态应该为\(f_i\)表示叠到\(i\)层的期望次数。但是稍微考虑一下,我们就会发现这个转移方程非常难想。所以我们考虑换一种状态,对原状态进行差分:设\(f_i\)表示从第\(i-1\)层叠到第\(i\)层的期望次数。
此时分三种情况:直接叠上去,坍塌到底层,坍塌了\(j\)层。其中第一种的期望值为1;第二种的期望值为\(p_i^i\sum_{j=1}^if_i\)(概率是\(p_i^i\),代价是\(\sum_{j=1}^if_i\));第三种的期望值为\(\sum_{j=1}^{i-1}(p_i^{j}(1-p_i)\sum_{k=i-j+1}^if_k)\)(概率是\(p_i^j(i-p_i)\)代价是\(\sum_{k=i-j+1}^if_k\))。
直接对三种情况线性相加,即:
\(f_i=1+p_i^i\sum_{j=1}^if_j+\sum_{j=1}^{i-1}(p_i^j(1-p_i)\sum_{k=i-j+1}^if_k)\)
\(\ \ =p_if_i+1+\sum_{j=2}^ip_i^jf_{i-j+1}\)
解得
\(f_i=\frac{1+\sum_{j=2}^ip_i^jf_i-j+1}{1-p_i}\)
方程化简到此为止,但此时的时间复杂度为\(O(n^2)\),显然会超时,所以我们考虑用前缀和优化\(sigma\)。因为\(p\)是不定的,但是\(p\)的取值仅限于\(1\)~\(99\)之间,所以我们用每一种\(p\)去求前缀和。设\(s_i=\sum_{j=2}^ip_i^jf_i-j+1\),那么\(s_{i+1}=p(s_i+p*f_i)\)。这样,总的时间复杂度就位\(O(n)\),常数为100,略大,但能过。
代码:
#include<iostream>
#define int long long
using namespace std;
const int N=2e5+5,mod=998244353,inv=828542813;
int n,p[105],ans,a[N],dp[N],s[105];
inline int qpow(int x,int y){
int res=1;
while(y){
if(y&1) res=(res*x)%mod;
x=(x*x)%mod;
y>>=1;
}
return res%mod;
}//快速幂
signed main(){
ios::sync_with_stdio(false);
cin>>n;
for(int i=1;i<=n;i++) cin>>a[i];
for(int i=0;i<100;i++) p[i]=i*inv%mod;//求出概率
for(int i=1;i<=n;i++){
dp[i]=(s[a[i]]+1)*qpow(1-p[a[i]]+mod,mod-2)%mod;//递推方程式
ans=(ans+dp[i])%mod;//求答案
for(int j=0;j<100;j++) s[j]=(s[j]*p[j]%mod+p[j]*p[j]%mod*dp[i])%mod;//每一个p都算一便
}cout<<ans<<'\n';//输出答案
return 0;//完结撒花~~
}//借鉴自:https://www.luogu.com.cn/article/iwh4skdn