2020.6.21
2020.6.21
2020.6.21考完月考的第一天还没出成绩 赶紧来加一篇
d
p
dp
dp
2020.10.13
2020.10.13
2020.10.13没报上
C
S
P
CSP
CSP要考联赛得用老师的推荐名额,所以赶紧回来好好学两个月拿个一等奖
现在就啥对联赛有用就学啥,先弄
d
p
dp
dp,再弄图论,最后整点数奆结垢和数学
先可简单的整吧
基本概念
大家应该都知道线性 d p dp dp,即在线性结构上进行状态转移,而区间类动态规划是线性动态规划的拓展,它在分阶段划分问题时,与阶段中元素出现的顺序和由前一阶段哪些元素合并而来有很大的关系。如状态 f [ i ] [ j ] f[i][j] f[i][j],它表示以 已合并的次数为阶段, 以 区卷左端点 i i i为状态,它的值取决于第 i i i个元素和第 j j j个元素断开的位置 k k k,即 f [ i ] [ k ] + f [ k + 1 ] [ j ] f[i][k]+f[k+1][j] f[i][k]+f[k+1][j]的值。这一类型的动态规划,阶段特征得长明显,求最优值时需要预先设置阶段内的区间统计值,还要以动态规划的起始位置来判断。
操作
区间
d
p
dp
dp一般只有两步操作
1.
1.
1.合并:即将两个或多个部分进行整合,当然也可以反过来,也就是对一个问题分解成两个或多个部分
2.
2.
2.求解:对整个问题设最优值,枚举合并点,将问题分解成左右两个部分,最后合并左右两个部分得最优值得到原问题的最优值。类似分治算法的思想。
所以一个问题能不能用区间 d p dp dp,就是要看这个问题是否能被分解成为两两合并的形式。
这块也没啥难的 直接上题
石子合并
题面
S
o
l
u
t
i
o
n
:
Solution:
Solution:这题做的时候注意一下石头是按圆形操场摆放的 不是排一排
那怎么转化为排成一排的呢 只需要把序列再复制一遍就可以了
转移方程根本不用推。。
最大时
d
p
[
i
]
[
j
]
=
m
i
n
k
=
i
j
−
1
{
d
p
[
i
]
[
k
]
+
d
p
[
k
+
1
]
[
j
]
+
∑
l
=
i
j
a
[
l
]
}
dp[i][j]=min_{k=i}^{j-1}\{dp[i][k]+dp[k+1][j]+\sum_{l=i}^{j}a[l]\}
dp[i][j]=mink=ij−1{dp[i][k]+dp[k+1][j]+∑l=ija[l]}
最小时
d
p
[
i
]
[
j
]
=
m
a
k
=
i
j
−
1
{
d
p
[
i
]
[
k
]
+
d
p
[
k
+
1
]
[
j
]
+
∑
l
=
i
j
a
[
l
]
}
dp[i][j]=ma_{k=i}^{j-1}\{dp[i][k]+dp[k+1][j]+\sum_{l=i}^{j}a[l]\}
dp[i][j]=mak=ij−1{dp[i][k]+dp[k+1][j]+∑l=ija[l]}
上代码:
#include<bits/stdc++.h>
using namespace std;
#define N 404
#define maxx 999999999
int n,ans,a[N],s[N],dp[N][N];
inline void read(int &x){
int s=0,w=1;char ch=getchar();
while(ch<'0'||ch>'9'){if(ch=='-')w=-1;ch=getchar();}
while(ch>='0'&&ch<='9'){s=(s<<3)+(s<<1)+(ch&15);ch=getchar();}
x=s*w;
}
int main(){
read(n);
for(int i=1;i<=n;i++)read(a[i]),a[i+n]=a[i];
for(int i=1;i<=n<<1;i++)s[i]=s[i-1]+a[i];
for(int l=1;l<n;l++){
for(int i=1,j=l+1;j<=n<<1;i++,j++){
dp[i][j]=maxx;
for(int k=i;k<j;k++)dp[i][j]=min(dp[i][j],dp[i][k]+dp[k+1][j]+s[j]-s[i-1]);
}
}
ans=maxx;
for(int i=1;i<=n;i++)ans=min(dp[i][i+n-1],ans);
printf("%d\n",ans);
for(int l=1;l<n;l++){
for(int i=1,j=l+1;j<=n<<1;i++,j++){
dp[i][j]=0;
for(int k=i;k<j;k++)dp[i][j]=max(dp[i][j],dp[i][k]+dp[k+1][j]+s[j]-s[i-1]);
}
}
ans=0;
for(int i=1;i<=n;i++)ans=max(dp[i][i+n-1],ans);
printf("%d\n",ans);
}
能量项链
题面
S
o
l
u
t
i
o
n
:
Solution:
Solution:果题 需要注意的是
i
−
k
i-k
i−k区间和
k
−
j
k-j
k−j区间合并时 加上的是
a
[
i
]
×
a
[
k
+
1
]
×
a
[
j
+
1
]
a[i]×a[k+1]×a[j+1]
a[i]×a[k+1]×a[j+1] 别乘错了!
状态转移方程:
d
p
[
i
]
[
j
]
=
m
a
x
k
=
i
j
−
1
{
d
p
[
i
]
[
k
]
+
d
p
[
k
]
[
j
]
+
a
[
i
]
×
a
[
k
+
1
]
×
a
[
j
+
1
]
}
dp[i][j]=max_{k=i}^{j-1}\{dp[i][k]+dp[k][j]+a[i]×a[k+1]×a[j+1]\}
dp[i][j]=maxk=ij−1{dp[i][k]+dp[k][j]+a[i]×a[k+1]×a[j+1]}
代码↓
#include<bits/stdc++.h>
using namespace std;
#define N 220
int n,ans,a[N],dp[N][N];
int main(){
scanf("%d",&n);
for(int i=1;i<=n;i++){
scanf("%d",&a[i]);
a[i+n]=a[i];
}
for(int l=1;l<n;l++){
for(int i=1,j=l+1;j<=n<<1;i++,j++){
for(int k=i;k<j;k++)dp[i][j]=max(dp[i][j],dp[i][k]+dp[k+1][j]+a[i]*a[k+1]*a[j+1]);
}
}
for(int i=1;i<=n;i++)if(dp[i][i+n-1]>ans)ans=dp[i][i+n-1];
printf("%d\n",ans);
}
凸多边形的划分
题面
S
o
l
u
t
i
o
n
:
Solution:
Solution:
先将节点顺时针依次编号
d
p
[
i
]
[
j
]
dp[i][j]
dp[i][j]表示从
i
i
i节点到
j
j
j节点划分得到的乘积之和最小值
如图
那么如何状态转移呢
设
k
∈
(
i
,
j
)
k∈(i,j)
k∈(i,j),将
i
−
j
i-j
i−j分成
i
−
k
,
k
−
j
i-k,k-j
i−k,k−j来转移
如图,在
k
k
k点分割之后出现了三个三角形
绿色部分分别为
d
p
[
i
]
[
k
]
dp[i][k]
dp[i][k]和
d
p
[
k
]
[
j
]
dp[k][j]
dp[k][j]
黄色部分为
a
[
i
]
×
a
[
j
]
×
a
[
k
]
a[i]×a[j]×a[k]
a[i]×a[j]×a[k]
那么
d
p
[
i
]
[
j
]
=
m
i
n
k
=
i
+
1
j
−
1
{
d
p
[
i
]
[
k
]
+
d
p
[
k
]
[
j
]
+
a
[
i
]
×
a
[
j
]
×
a
[
k
]
}
dp[i][j]=min_{k=i+1}^{j-1}\{dp[i][k]+dp[k][j]+a[i]×a[j]×a[k]\}
dp[i][j]=mink=i+1j−1{dp[i][k]+dp[k][j]+a[i]×a[j]×a[k]}
初始状态
d
p
[
i
]
[
j
]
=
m
a
x
dp[i][j]=max
dp[i][j]=max,
d
p
[
i
]
[
i
+
1
]
=
0
dp[i][i+1]=0
dp[i][i+1]=0
需要注意的是不开高精只有50分
q
a
q
qaq
qaq
就当打高精模板了
上代码
#include<bits/stdc++.h>
using namespace std;
#define N 55
#define ll long long
ll n,a[N];
struct bigint{
int len,s[303];
bigint(){
memset(s,0,sizeof(s));
len=1;
}
bool operator <(const bigint &A)const{
if(len!=A.len)return len<A.len;
for(int i=len-1;~i;i--)
if(s[i]!=A.s[i])return s[i]<A.s[i];
return false;
}
bigint operator +(const bigint &A)const{
bigint B;
B.len=max(len,A.len);
for(int i=0;i<B.len;i++){
B.s[i]+=s[i]+A.s[i];
B.s[i+1]+=B.s[i]/10,B.s[i]%=10;
}
if(B.s[B.len])B.len++;
return B;
}
bigint operator *(const bigint &A)const{
bigint B;
B.len=len+A.len-1;
for(int i=0;i<A.len;i++){
for(int j=0;j<len;j++){
B.s[i+j]+=A.s[i]*s[j];
B.s[i+j+1]+=B.s[i+j]/10,B.s[i+j]%=10;
}
}
if(B.s[B.len])B.len++;
return B;
}
void print(){
int now=len-1;
while(!s[now]&&now)now--;
for(int i=now;~i;i--)printf("%d",s[i]);
}
}dp[N][N];
bigint change(ll x){
bigint t;int cnt=0;
while(x){
t.s[cnt++]=x%10;
x/=10;
}
t.len=cnt;
return t;
}
int main(){
cin>>n;
for(int i=1;i<=n;i++)cin>>a[i];
for(int l=2;l<n;l++){
for(int i=1,j=l+1;j<=n;i++,j++){
dp[i][j].len=300,dp[i][j].s[299]=1;
for(int k=i+1;k<j;k++){
bigint t=change(a[i])*change(a[j])*change(a[k]);
bigint newdp=dp[i][k]+dp[k][j]+t;
if(newdp<dp[i][j])dp[i][j]=newdp;
}
}
}
dp[1][n].print();
}
括号配对
题面
S
o
l
u
t
i
o
n
:
Solution:
Solution:判断需要最少加几个括号就是找最少有多少个括号没匹配
逆向思维直接找能匹配的最多的括号
可以直接用
S
T
L
STL
STL栈过 还是练下区间
d
p
dp
dp吧
用
d
p
[
i
]
[
j
]
dp[i][j]
dp[i][j]表示
i
−
j
i-j
i−j区间内可以匹配到的最大括号数量
在序列
i
−
j
i-j
i−j中 如果
s
[
i
]
s[i]
s[i]和
s
[
j
]
s[j]
s[j]可以匹配,则
d
p
[
i
]
[
j
]
=
d
p
[
i
+
1
]
[
j
−
1
]
+
2
dp[i][j]=dp[i+1][j-1]+2
dp[i][j]=dp[i+1][j−1]+2
之后再直接判断
d
p
[
i
]
[
j
]
=
m
a
x
k
=
i
j
−
1
{
d
p
[
i
]
[
k
]
+
d
p
[
k
]
[
j
]
}
dp[i][j]=max_{k=i}^{j-1}\{dp[i][k]+dp[k][j]\}
dp[i][j]=maxk=ij−1{dp[i][k]+dp[k][j]}
上代码
#include<bits/stdc++.h>
using namespace std;
int n,dp[110][110];
char s[110];
int main(){
gets(s);
n=strlen(s);
for(int l=1;l<n;l++){
for(int i=0,j=l;j<n;i++,j++){
if((s[i]=='('&&s[j]==')')||(s[i]=='['&&s[j]==']'))dp[i][j]=dp[i+1][j-1]+2;
for(int k=i;k<j;k++)dp[i][j]=max(dp[i][j],dp[i][k]+dp[k+1][j]);
}
}
cout<<n-dp[0][n-1]<<endl;
}
分离与合体
题面
S
o
l
u
t
i
o
n
:
Solution:
Solution:这题题面不太好懂,先看第一问,大概意思就是说一个序列,每两个数可以合并成一个数,问最后合并的数最大是多少
很显然的区间
d
p
dp
dp,转移方程一眼就能看出来
d
p
[
i
]
[
j
]
=
m
a
x
k
=
i
j
−
1
{
d
p
[
i
]
[
k
]
+
d
p
[
k
−
1
]
[
j
]
+
(
a
[
i
]
+
a
[
j
]
)
×
a
[
k
]
}
dp[i][j]=max_{k=i}^{j-1}\{dp[i][k]+dp[k-1][j]+(a[i]+a[j])×a[k]\}
dp[i][j]=maxk=ij−1{dp[i][k]+dp[k−1][j]+(a[i]+a[j])×a[k]}
但是问题是第二问,如何将每次分裂记下来
设
p
[
i
]
[
j
]
p[i][j]
p[i][j]是
i
i
i到
j
j
j区间分裂的位置,只需在每次更新
d
p
[
i
]
[
j
]
dp[i][j]
dp[i][j]时顺便维护
p
p
p数组
即当
d
p
[
i
]
[
k
]
+
d
p
[
k
−
1
]
[
j
]
+
(
a
[
i
]
+
a
[
j
]
)
×
a
[
k
]
>
d
p
[
i
]
[
j
]
dp[i][k]+dp[k-1][j]+(a[i]+a[j])×a[k]>dp[i][j]
dp[i][k]+dp[k−1][j]+(a[i]+a[j])×a[k]>dp[i][j]时 让
p
[
i
]
[
j
]
=
k
p[i][j]=k
p[i][j]=k即可
输出时用搜索来找每一层分裂的位置,因为除第一层外有很多个分裂点,所以用
v
e
c
t
o
r
vector
vector来存储每一层分裂的位置,最后再排序输出
需要注意的是 最后一个数不要输出空格 不然会格式错误…
上代码
#include<bits/stdc++.h>
using namespace std;
#define N 330
int n,dep,a[N],dp[N][N],p[N][N];
vector<int> ans[N];
void dfs(int l, int r, int depth){
if(l==r)return ;
if(depth>dep)dep=depth;
ans[depth].push_back(p[l][r]);
dfs(l,p[l][r],depth+1);
dfs(p[l][r]+1,r,depth+1);
}
int main(){
cin>>n;
for(int i=1;i<=n;i++)cin>>a[i];
for(int l=1;l<n;l++){
for(int i=1,j=l+1;j<=n;i++,j++){
for(int k=i;k<j;k++){
int now=dp[i][k]+dp[k+1][j]+(a[i]+a[j])*a[k];
if(now>dp[i][j])dp[i][j]=now,p[i][j]=k;
}
}
}
cout<<dp[1][n]<<endl;
dfs(1,n,1);
for(int i=1;i<=dep;i++)sort(ans[i].begin(),ans[i].end());
for(int i=1;i<=dep;i++){
int siz=ans[i].size();
for(int j=0;j<siz;j++){
if(j==siz-1&&i==dep)printf("%d",ans[i][j]);
else printf("%d ",ans[i][j]);
}
}
cout<<endl;
}
矩阵取数游戏
题面
S
o
l
u
t
i
o
n
:
Solution:
Solution:这题还是个
N
O
I
P
NOIP
NOIP题哈哈
每行取的数只与本行之前的方案有关,所以我们只需要让每一行都取最优
设
d
p
[
i
]
[
j
]
dp[i][j]
dp[i][j]表示每行处理
i
−
j
i-j
i−j时,当前的最优方案
则
d
p
[
i
]
[
j
]
=
m
a
x
{
d
p
[
i
−
1
]
[
j
]
+
2
m
−
i
−
j
+
1
,
d
p
[
i
]
[
j
+
1
]
+
2
m
−
i
−
j
+
1
}
dp[i][j]=max\{dp[i-1][j]+2^{m-i-j+1},dp[i][j+1]+2^{m-i-j+1}\}
dp[i][j]=max{dp[i−1][j]+2m−i−j+1,dp[i][j+1]+2m−i−j+1}
答案需要在区间为
1
1
1的
d
p
[
i
]
[
i
]
dp[i][i]
dp[i][i]里找
a
n
s
=
∑
k
=
1
n
m
a
x
i
=
1
m
{
d
p
[
i
]
[
i
]
+
2
i
×
a
[
i
]
}
ans=\sum\limits_{k=1}^{n}max_{i=1}^{m}\{dp[i][i]+2^i×a[i]\}
ans=k=1∑nmaxi=1m{dp[i][i]+2i×a[i]}
预处理出
2
n
2^n
2n,再打个高精 就可以了
代码在机房 不想再打一次高精了
(
(
(哭哭 周二更代码
周一中午跑来交代码了哈哈 我
D
a
w
n
Dawn
Dawn绝不鸽!
上代码↓
#include<bits/stdc++.h>
using namespace std;
#define reg register
#define N 82
struct bigint{
int s[301],len;
bigint(){
memset(s,0,sizeof(s));
len=1;
}
bool operator <(const bigint &A)const{
if(len!=A.len)return len<A.len;
for(reg int i=len-1;~i;i--)
if(s[i]!=A.s[i])return s[i]<A.s[i];
return false;
}
bigint operator +(const bigint &A)const{
bigint B;
B.len=max(len,A.len);
for(int i=0;i<B.len;i++)
B.s[i]+=s[i]+A.s[i],B.s[i+1]+=B.s[i]/10,B.s[i]%=10;
if(B.s[B.len])B.len++;
return B;
}
bigint operator *(const bigint &A)const{
bigint B;
B.len=len+A.len-1;
for(int i=0;i<A.len;i++)
for(int j=0;j<len;j++)
B.s[i+j]+=A.s[i]*s[j],B.s[i+j+1]+=B.s[i+j]/10,B.s[i+j]%=10;
if(B.s[B.len])B.len++;
return B;
}
void print(){
int now=len-1;
while(!s[now]&&now)now--;
for(reg int i=now;~i;i--)putchar(s[i]+'0');
putchar(10);
}
}dp[N][N],bas[N],cnt;
void work(){
bas[0].len=1,bas[0].s[0]=1;
bigint t;t.len=1,t.s[0]=2;
for(reg int i=1;i<=80;i++)bas[i]=bas[i-1]*t;
}
bigint change(int x){
int sum=0;
bigint num;
while(x){
num.s[sum++]=x%10;
x/=10;
}
num.len=sum;
return num;
}
int n,m,a[N];
int main(){
cin>>n>>m;
work();
for(reg int f=1;f<=n;f++){
for(int i=1;i<=m;i++){
for(int j=1;j<=m;j++)
dp[i][j].len=1,dp[i][j].s[0]=0;
}
for(reg int i=1;i<=m;i++)scanf("%d",&a[i]);
for(reg int i=1;i<=m;i++)
for(reg int j=m;j>=i;j--){
bigint p=change(a[i-1]),q=change(a[j+1]);
bigint s1=dp[i-1][j]+p*bas[m+i-j-1],s2=dp[i][j+1]+q*bas[m-j+i-1];
if(dp[i][j]<s1)dp[i][j]=s1;
if(dp[i][j]<s2)dp[i][j]=s2;
}
bigint ans;
for(reg int i=1;i<=m;i++){
bigint p=change(a[i]);
if(ans<p*bas[m]+dp[i][i])ans=p*bas[m]+dp[i][i];
}
cnt=cnt+ans;
}
cnt.print();putchar(10);
}
总结
区间 d p dp dp的复杂度一般都是 O ( n 3 ) O(n^3) O(n3),故数据范围在 300 300 300左右时可以考虑区间 d p dp dp,区间 d p dp dp的状态转移方程好想,总体来说是个不错的解题方法。