2022做题记录
- 【变形背包DP】LGP2340 [USACO03FALL]Cow Exhibition G
- 【区间dp】【破环成链】[NOI1995]石子合并
- 【区间dp】LGP3146 [USACO16OPEN]248 G
- 【区间dp】【破环成链】LGP1063 [NOIP2006 提高组] 能量项链
- 【破环成链】【区间dp】LGP4342 [IOI1998]Polygon
- 【斜率优化】BZOJ3437 小P的牧场
- 【序列分段】【区间dp】[UVA12991] Game Rooms
- 【区间dp】[USACO12MAR]Cows in a Skyscraper G
- 【平衡树】【线段树】P3391 【模板】文艺平衡树
- 【线性dp】P1018 [NOIP2000 提高组] 乘积最大
- 【EXCRT】【平衡树】【exgcd】[NOI2018] 屠龙勇士 题解
【变形背包DP】LGP2340 [USACO03FALL]Cow Exhibition G
题意
每个物品有两个属性,求选一些物品使得这两个属性之和最大,且满足任何一个属性的和都非负。
思路
先考虑让两个属性的和最大。
先考虑一个定下来之后另一个属性和最大。
那么就相当于01背包里面容量定下来,权值和最大。
那么我们就可以把智商和情商看做是容量和价值
于是乎我们就有\(\displaystyle dp_{i}=\max \{dp_{i-a_k}+b_k\}\)
然后我们对于所有的\(i\)求\(ans=\displaystyle\max_{i\in[1,n]}\{i+dp_i\}\)
注意到转移过程中智商可以是负的,所以考虑把数组整体右移。
Code
#include <algorithm>
#include <cstring>
#include <cstdio>
#define mxn (410000)
int dp[mxn<<1],a[401],b[401];
int n,sum,ans;
int main()
{
scanf("%d",&n);
for(register int i=1;i<=n;++i) scanf("%d%d",&a[i],&b[i]);
for(register int i=1;i<=n;++i) if(a[i]>0) sum+=a[i];
sum<<=1;
std::memset(dp,0x80,sizeof(dp));
dp[sum>>1]=0;
for(register int i=1;i<=n;++i)
{
if(a[i]>=0)
{
for(register int j=sum;j>=a[i];--j)
{
dp[j]=std::max(dp[j],dp[j-a[i]]+b[i]);
}
}else
{
for(int j=0;j<=sum-a[i];++j)
{
dp[j]=std::max(dp[j],dp[j-a[i]]+b[i]);
}
}
}
sum>>=1;
for(register int i=0;i<=sum;++i)
{
if(dp[i+sum]>=0) ans=std::max(ans,i+dp[i+sum]);
}
printf("%d\n",ans);
return 0;
}
【区间dp】【破环成链】[NOI1995]石子合并
题意
一堆石头围成一圈,每次拿两堆合并,分数为新堆的石子数。
求合成一堆的最小分数和最大分数。
思路
区间合并,所以考虑区间dp
考虑使用\(dp_{i,j}\)表示把区间\([i,j]\)合并的最小花费(最大同理
那么这个一定是由更小的区间合成的。
我们考虑枚举这些区间
然后他又是一个环,我们破环成链,在后面复制一份。
最后的答案就是\(\displaystyle \min_{1\leq i\leq n}\{dp_{i,i+n-1}\}\)
然后跑一遍最大就可以了
Code
#include <cstdio>
#include <algorithm>
#include <cstring>
const int N=110;
int dp[N<<1][N<<1],a[N<<1],sum[N<<1];
int n,mxn;
int solve(int (*func)(int,int))
{
std::memset(dp,(~func(0x80<<24,0x7f<<24))>>25,sizeof(dp));
for(register int i=1;i<=mxn;++i) dp[i][i]=0;
for(register int R=1;R<n;++R)
{
for(register int i=1;i+R<=mxn;++i)
{
const register int j=i+R;
if(j>=mxn) break;
for(register int k=i;k<j;++k)
{
dp[i][j]=func(dp[i][j],dp[i][k]+dp[k+1][j]);
}
dp[i][j]+=sum[j]-sum[i-1];
}
}
int res=dp[1][n];
for(register int i=2;i<=n;++i) res=func(res,dp[i][i+n-1]);
return res;
}
int min(int a,int b) { return a<b? a:b; }
int max(int a,int b) { return a>b? a:b; }
int main()
{
scanf("%d",&n);
mxn=n<<1;
for(register int i=1;i<=n;++i)
{
scanf("%d",&a[i]);
a[i+n]=a[i];
}
for(register int i=1;i<=mxn;++i) sum[i]=sum[i-1]+a[i];
printf("%d\n",solve(min));
printf("%d\n",solve(max));
return 0;
}
【区间dp】LGP3146 [USACO16OPEN]248 G
一条链长\(2\leq N \leq 248\),总有\(a_i\in [1,40]\quad (i\in[1,N])\)
给定初始的序列,每次可以合并相邻的两个相同的数使他们变成这个数加一。
求若干次合并后序列中最大值最大是多少。
输入 #1
4
1
1
1
2
输出 #1
3
#1 解释
很明显把2号位和3号位合成为2,然后和4号位合成为3是最大的。
思路
题意已经很清楚了所以不讲了
区间状压DP
考虑使用\(dp_{i,j,k}(1\leq i < j\leq N+1,k\in[1,40])\)来表示区间\([i,j)\)能否合并为\(k\)
然后输出的时候遍历一次\(dp_{1,n}\)
时间复杂度\(O(kN^4)\)打死都过不了
回来想想正确性也不对劲。
如果我不能合成为一个数呢?我还剩俩不一样的。
而且我们\(dp\)的值一直都没有用过。我们可以考虑用\(dp_{i,j}\)表示把\([i,j)\)全部合并成的值。如果没有就给0。
转移?
中转点
答案?
看上去可以。
那么转移顺序?这个很关键,转移顺序不对劲什么都搞不出来。
首先考虑一下,我们是区间合并,所以要先整出小区间,然后再合并成大区间。
所以第一关键字应该是区间长度升序。
然后第二关键字就可以是区间起点升序,这样就确定了一个区间。
然后第三关键字就是中转点升序。
Code
#include <algorithm>
#include <cstdio>
using namespace std;
const int N=256;
int a[N], dp[N][N];
int n,ans;
int main()
{
scanf("%d",&n);
for(register int i=1;i<=n;++i) scanf("%d",&dp[i][i+1]);
for(register int i=1;i<=n;++i) ans=max(ans,dp[i][i+1]);
for(register int len=2;len<=n;++len)
{
for(register int i=1;i<=n-len+1;++i)
{
register const int j=i+len;
for(register int k=i+1;k<=j;++k)
{
if(dp[i][k]==dp[k][j])
{
dp[i][j]=max(dp[i][j],dp[i][k]+1);
ans=max(dp[i][j],ans);
}
}
}
}
printf("%d\n",ans);
return 0;
}
【区间dp】【破环成链】LGP1063 [NOIP2006 提高组] 能量项链
题意
每个物品有两个属性\(x_i,y_i\leq 1000\),保证\(\forall i \in [1,n),y_i=x_{i+1}, y_n=x_1\)
每次可以合并两个物品\(i,i+1\)或者\(1,-1\)(就是最后一个和第一个),新物品仍然放在旧物品的位置上,属性分别是\(x_i,y_{i+1}\)或\(x_{-1},y_{1}\)
这次合并得到的分数是\(x_i\times (y_i=x_{i+1})\times y_{i+1}\)或\(x_{-1}\times(y_{-1}=x_1)\times y_1\)
现在给定\(4\leq N \leq 100\)个物品,求合并成一个物品的最大得分\(E\leq 2.1\times 10^9\)
思路
又是合并问题,区间合并成一个,可以考虑区间dp
定义\(dp_{i,j}\)表示把区间\([i,j)\)的物品合成的最大得分。
- 无后效性:
反正这个区间都合成一个了,后续不可能对原区间内的物品再进行操作。 - 子问题最优决策:
枚举中转点,那么这次合并的得分就已经知道了。只要中转点分开的两个区间得分各自最大就可以了。
根据题目我们很容易就可以得到状态转移方程
初始状态,全0就好
结束状态,\(dp_{1,n+1}\)
所以说我们根本就不需要\(y\)这个属性,如果我们采用左闭右开的区间表示方法。
所以给的输入格式也很开心。
转移顺序,一样的区间合并的题目。
第一关键字:区间长度升序\(len:[2,n]\)(就一个你合成什么合)
第二关键字:区间起点升序\(i:[1,n-len+1]\),可以求出区间终点\(j=i+len\)
第三关键字:中转点升序\(k:(i,j)\)(因为两边都要有才能合,所以两边都不能取)
注意到题目中描述的是一个环,所以需要破环成链,把数据复制一份到后面,最后的答案就是\(\max_{0<i<n}\{dp_{1+i,n+i+1}\}\)
Code
#include <algorithm>
#include <iostream>
using namespace std;
const int N=256;
int dp[N][N],x[N];
int n,ans,nn;
int main()
{
cin>>nn;
for(register int i=1;i<=nn;++i) cin>>x[i];
n=nn<<1;
for(register int i=nn+1;i<=n;++i) x[i]=x[i-nn];
for(register int len=2;len<=n;++len)
{
for(register int i=1;i<=n-len+1;++i)
{
const int j=i+len;
for(register int k=i+1;k<j;++k)
{
dp[i][j]=max(dp[i][j],dp[i][k]+dp[k][j]+x[i]*x[k]*x[j]);
}
}
}
for(register int i=0;i<nn;++i) ans=max(ans,dp[1+i][nn+i+1]);
cout<<ans<<endl;
return 0;
}
【破环成链】【区间dp】LGP4342 [IOI1998]Polygon
【斜率优化】BZOJ3437 小P的牧场
【序列分段】【区间dp】[UVA12991] Game Rooms
【区间dp】[USACO12MAR]Cows in a Skyscraper G
题意
给出n个物品,体积为w[i],现把其分成若干组,要求每组总体积<=W,问最小分组。(n<=18)
思路
打表
状压dp都有一个特点就是一般\(n\)会给特别小(不然你也压不下来)
主要思路就是用一个数字来表示状态,比如我们可以用\((000100)_2\)来表示第三个物品(从右到左)已经被选了,其他物品没有被选。
于是乎我们就可以从\(i\)转移到i|(1<<j)
条件是(i&(1<<j)==0)
,就是物品在当前状态下是没有被选的。
那么我们要不要新开一组呢?这就取决于我们上一组放了多少物品。我们可以开一个数组\(g_i\)表示状态\(i\)下剩余空间最多的一组的剩余空间。然后一比对就可以了。
Code
#include <cstring>
#include <iostream>
#include <cstdio>
using namespace std;
int n,w,dp[1<<18],g[1<<18],c[20];
int main()
{
memset(dp,0x7f,sizeof(dp));
scanf("%d%d",&n,&w);
for(register int i=0;i<n;++i) scanf("%d",&c[i]);
dp[0]=1; g[0]=w;
for(register int i=0;i<1<<n;++i)
{
for(register int j=0;j<n;++j)
{
if(i&(1<<j)) continue;
else
{
const int opt=i|(1<<j);
if(g[i]>=c[j] and dp[opt]>=dp[i])
{
dp[opt]=dp[i];
g[opt]=max(g[opt],g[i]-c[j]);
}else if(g[i]<c[j] and dp[opt]>=dp[i]+1)
{
dp[opt]=dp[i]+1;
g[opt]=max(g[opt],w-c[j]);
}
}
}
}
cout << dp[(1<<n)-1] << endl;
return 0;
}
【平衡树】【线段树】P3391 【模板】文艺平衡树
题意
维护区间反转。\(n,q\leq 10^5\)
思路
说到区间操作,本能就想起来使用线段树
脑子里先回忆一下线段树的细节,通过把区间拆成\(\log_2n\)个区间,并且使用标记来降低复杂度。
但是线段树有个缺点就是一个结点管辖范围是确定的,而区间反转这种操作是很灵活的。比如结点管辖\([1,2]\)而我们要翻转\([1,3]\)就好像很难操作。
所以我们要考虑对线段树进行一下变形的魔改文艺化修饰。
首先考虑一下怎样会使得区间反转操作看上去很方便。对于一个线段树的结点,如果我翻转的范围刚好是他管辖的范围,那么我们只需要从上到下对于每一个点都进行一次左右子树的交换,就可以做到该点内的区间反转。
除了叶子结点的访问顺序反转了之外,我们也意外的发现这个结点下的中序遍历也刚好反转了!
那么,我们能不能用中序遍历来表示点对应原序列的位置呢?比如说该树的中序遍历是5 4 2 3 1
,就意味着反转完成后的序列是\(a_5,a_4,a_2,a_3,a_1\)。这是完全可以的。
为什么要这样?因为我们学过的平衡树,也就是二叉搜索树,因为他的性质,可以做到在旋转的时候不破坏中序遍历的顺序(破坏了还能当二叉搜索树么)
然后旋转又可以改变树的结构,因此我们就可以通过旋转把需要的区间转到一个结点下。
具体怎么操作呢?我们可以很明显知道如果根结点是\(x\),那么左子树就是\([1,x)\),右子树就是\((x,n]\)。\([l,r]\)区间反转时,我们就可以先把\(l-1\)旋上根,然后把\(r+1\)旋到根的右儿子,那么\(r+1\)的左儿子,们,就是\([l,r]\)。打上标记就好。
注意在对一个结点进行旋转之前要将起父结点和当前结点的标记都下传,以及建区间时左右两边多定义一个结点以便反转\([1,n]\)
Code
#include <cstdio>
#define ls sn[0]
#define rs sn[1]
#ifndef ONLINE_JUDGE
#define scanf(x...) fscanf(infile,x)
#define printf(x...) fprintf(outfile,x)
FILE* infile=fopen("literary.in","r");
FILE* outfile=fopen("literary.out","w");
#endif // ONLINE_JUDGE
struct splay
{
splay* fa,*sn[2];
bool tag;
size_t sze;
int dat;
splay(splay* f=0x0, splay* s=0x0) { fa=f; sn[0]=sn[1]=s; sze=0; tag=false; }
inline int chk() const { return this==fa->rs; }
}*rt;
splay* const None=new splay;
int n,m,l,r;
inline size_t maintain(splay* x) { return x->sze=x->ls->sze+x->rs->sze+1; }
size_t build(splay* k,int l,int r)
{
k->dat=(l+r)>>1;
if(l<k->dat) build(k->ls=new splay(k,None),l,k->dat-1);
if(k->dat<r) build(k->rs=new splay(k,None),k->dat+1,r);
return maintain(k);
}
template<typename T>
inline void swap(T &a,T &b) { T c=a; a=b; b=c; return; }
inline void init()
{
scanf("%d%d",&n,&m);
None->ls=None->rs=None->fa=None;
build(rt=new splay(None,None),0,n+1);
}
inline void Rever(splay* x)
{
x->tag^=1;
swap(x->ls,x->rs);
}
inline void pushdown(splay* x)
{
if(x->tag)
{
Rever(x->ls); Rever(x->rs);
x->tag=0;
}
}
inline size_t rotate(splay* x)
{
if(x==None) return maintain(x);
if(x->fa==None) return maintain(x);
splay *y=x->fa; const int chk=x->chk();
pushdown(y); pushdown(x);
x->sn[chk^1]->fa=y; y->sn[chk]=x->sn[chk^1];
x->fa=y->fa; y->fa->sn[y->chk()]=x;
x->sn[chk^1]=y; y->fa=x;
None->ls=None->rs=None;
maintain(y); return maintain(x);
}
inline splay* up(splay* x, splay*top=None)
{
if(x==None) return x;
if(x==top) return x;
while(x->fa!=top)
{
if(x->fa->fa!=top) rotate(x->chk()==x->fa->chk()? x->fa:x);
rotate(x);
}
if(top==None) rt=x;
return x;
}
size_t rank(splay* x)
{
size_t res=x->ls->sze+1;
while(x!=None)
{
if(x->chk()) res+=x->fa->ls->sze+1;
x=x->fa;
}
return res;
}
inline splay* find(size_t k)
{
splay *p=rt;
while(p!=None)
{
pushdown(p);
if(k<=p->ls->sze) p=p->ls;
else if(k<=p->ls->sze+1) return p;
else if((k-=p->ls->sze+1)<=p->rs->sze) p=p->rs;
else return None;
}
return None;
}
inline void reverse(int l,int r)
{
if(l>r) return;
up(find(l));
up(find(r+2),rt);
Rever(rt->rs->ls);
}
void print(splay *x)
{
if(x==None) return;
pushdown(x);
print(x->ls);
printf("%d ",x->dat);
print(x->rs);
}
int main()
{
init();
while(m--)
{
scanf("%d%d",&l,&r);
reverse(l,r);
}
up(find(1));
up(find(n+2),rt);
print(rt->rs->ls);
#ifndef ONLINE_JUDGE
#undef scanf
#undef printf
fclose(infile); fclose(outfile);
#endif // ONLINE_JUDGE
return 0;
}
【线性dp】P1018 [NOIP2000 提高组] 乘积最大
题意
在一个自然数内插入一定个数的乘号,求最后表达式的最大值
思路
考虑到每个数后面只有插入/不插入两种状态
可以很轻易设计出状态\(dp_{i,j}\)表示前\(i\)个数字中插入了\(j\)个乘号的最大结果,然后状态转移就
- 从\(dp_{i-1,j}\)转移到\(dp_{i,j}\)(不加乘号)
- 从\(dp_{k,j}*s_{k+1,i}\)转移到\(dp_{i,j+1}\)(加乘号)
可以发现,这样的方案满足最优子结构和无后效性原则,而我们在设计状态转移的时候就必须保证子问题重叠性,因此这是一个合格的动态规划!
Code
#include<cstdio>
#include<algorithm>
#include<cstring>
int n,K;
struct Number
{
int dat[0x100];
Number(){ memset(dat,dat[0]=0,sizeof(dat)); }
Number(const Number& x) { memcpy(dat,x.dat,sizeof(dat)); }
Number operator +=(const Number x)
{
if(dat[0]<x.dat[0]) dat[0]=x.dat[0];
for(register int i=dat[0]++;i;--i) dat[i]+=x.dat[i];
for(register int i=1;i<=dat[0];++i) if(dat[i]>9) { dat[i]-=10; dat[i+1]+=1; }
while(dat[0] and dat[dat[0]]==0) --dat[0];
return *this;
}
Number operator + (const Number x) const
{
Number c=*this;
return c+=x;
}
Number operator *(const Number x) const
{
Number c; c.dat[0]=x.dat[0]+dat[0]+1;
for(register int i=1;i<=dat[0];++i) for(register int j=1;j<=x.dat[0];++j) c.dat[i+j-1]+=dat[i]*x.dat[j];
for(register int i=1;i<=c.dat[0];++i)
{
c.dat[i+1]+=c.dat[i]/10; c.dat[i]%=10;
}
while(c.dat[0] and c.dat[c.dat[0]]==0) --c.dat[0];
return c;
}
Number operator *=(const Number x)
{
return *this=*this*x;
}
bool operator < (const Number x) const
{
if(x.dat[0]==dat[0])
{
for(register int i=dat[0];i;--i)
{
if(x.dat[i]!=dat[i]) return dat[i]<x.dat[i];
}
}else return x.dat[0]>dat[0];
return false;
}
bool operator > (const Number x) const { return x<*this; }
bool operator == (const Number x) const { return !(x<*this || *this<x); }
inline void print() const
{
if(dat[0]==0)
{
printf("0\n"); return ;
}
for(register int i=dat[0];i;--i) printf("%d",dat[i]);
return ;
}
}dp[41][7];
char str[64];
Number cut(int l,int r)
{
Number c;
c.dat[0]=r-l;
for(int i=l;i<r;++i) c.dat[c.dat[0]-i+l]=str[i]-48;
return c;
}
int main()
{
scanf("%d%d",&n,&K);
scanf("%s",str);
dp[0][0]=cut(0,1);
for(register int i=1;i<n;++i)
{
dp[i][0]=cut(0,i+1);
for(register int k=0;k<K;++k)
{
for(register int j=0;j<i;++j)
{
dp[i][k+1]=std::max(dp[i][k+1],dp[j][k]*cut(j+1,i+1));
}
}
}
dp[n-1][K].print();
return 0;
}