标签:http noi 还需要 避免 假设 大会 hnoi 四边形不等式 长度
本来是打算写完背包再写区间 \(DP\) 的,但是发现,好像区间 \(DP\) 耗费的时间不是太长,在机房的时间也不是十分充沛,所以先写一波区间 \(DP\)。
你能学到什么
区间 \(DP\) 显然是以区间作为动态规划的阶段。去分成多个区间来搞。
这类大部分是有一个十分套路化的做法 :
for(qwq int len = 2 ; len <= n ; len++ ) //枚举区间长度
{
for(qwq int l = 1 ; l + len - 1 <= n ; l++) // 枚举右端点
{
int r = l + len - 1 ; // 右端点
for(qwq int k = l ; k < r ; k++)
{
f[l][r] = … …
}
}
}
这种方法的更新则是显然的。
那一定是需要枚举断点吗? 显然是不一定的。
我们考虑一下 \([l,r]\) 这个区间怎么扩展,或者说,怎么更新。我们可以换一种思路,这个区间可能是由 下图扩展而来的。使蓝色变成红色
那么很简单的意思,就是你的这个区间不一定非要是 \([l,r]\) 的断点转移你才满意,虽然说 \(l+1\) 也是一个断点,但是这种方法就避免了你去遍历断点,从而在两个断点间实现最优的转移。本文中 【只取一端的套路】 可以说明这种的正确性。
这里提这个作用是为了能够开阔一下脑袋瓜,省的学死了。
做个题就知道的方法,断环成链,就是将一个本质上是环的 \(DP\) ,将其从 \(1,n\) 再进行复制一遍,我们能够得到一个长度为 \(2n\) 的一条链,我们知道这条链可以代替那个环,其正确性就是环能够顺时针或逆时针找到一个节点,那么这条链也能够到达。正确性没什么问题。
我们将其划分为区间的合并的时候,有时候是比较像递归的,但是递归是重复次数特别多的,记忆化搜索就不会,所以有时候在实行区间 \(DP\) 的时候,思想,动态规划用的区间 \(DP\),但是实现的时候用的是记忆化搜索之类的。
老题了,老生常谈的板子题。
现有 \(n\) 个数组成一个环,求解将其合并为 \(1\) 个数的最大得分和最小得分。 只能合并相邻的两数,并且合并两数的得分为两个数之和。
这题本来是需要四边形不等式的,但是因为数据水,就直接暴力区间 \(DP\) 过了,为了防止在 \(T1\) 就死了,这里不给出四边形不等式的做法。并且这里直接给出区间 \(DP\) , 就不说错误的贪心了。
我们设 \(f_{i,j}\) 表示从 \(i\to j\) 的最小得分,那么我们考虑一下其中的断点会怎么给他转移。
那么显然我们是有 \(f_{i,j} = \min_{k\in[i,j]}f_{i,k}+f_{k+1,j} + Sum(i\to j)\)
int n ;
int a[kmaxn] , f1[kmaxn][kmaxn] , f2[kmaxn][kmaxn] , sum[kmaxn]; // max,min
signed main() {
n = read() ;
for(qwq int i = 1 ; i <= n ; i++) a[i] = a[i + n] = read() ;
for(qwq int i = 1 ; i <= n + n ; i++) sum[i] = sum[i - 1] + a[i] ;
for(qwq int i = 2 ; i <= n ; i++)
{
for(qwq int l = 1 , r ; l + i - 1 < n + n ; l++)
{
r = l + i - 1 ; f1[l][r] = - inf ; f2[l][r] = inf ;
for(qwq int k = l ; k < r ; k++)
{
f1[l][r] = std::max(f1[l][k] + f1[k + 1][r] + sum[r] - sum[l - 1] , f1[l][r]) ;
f2[l][r] = std::min(f2[l][k] + f2[k + 1][r] + sum[r] - sum[l - 1] , f2[l][r]) ;
}
}
}
int ans1 = inf , ans2 = - inf ;
for(qwq int i = 1 ; i <= n ; i++)
{
ans1 = std::min(f2[i][i + n - 1] , ans1) ;
ans2 = std::max(f1[i][i + n - 1] , ans2) ;
}
printf("%lld\n%lld" , ans1 , ans2) ;
return 0 ;
}
这个问题和上面的有点像,准确的来说,这个更为简单一点,但还是决定将其放在第二位上,而非作为第一个题。
在一个序列里玩合并,只有相邻的两个并且相等才可以合并,合并造成的影响就是将数 \(+ 1\) , 求解在整个合并过程中最大数。和 \(2048\) 不一样, \(2048\) 直接翻倍。
这个题很显然的比较简单,我们仍是设 \(f_{i,j}\) 表示能够合并区间 \(i\to j\) 的数。我们的状态转移方程也就是
就是相等(能够合并),取最大值,因为 \(1,n\) 不一定能够完全合并完 , 所以用一个变量取出来就好。
int f[kmaxn][kmaxn] , ans;
signed main() {
int n = read() ;
for(qwq int i = 1 ; i <= n ; i++) f[i][i] = read() ;
for(qwq int i = 2 ; i <= n ; i++)
{
for(qwq int l = 1 , r ; l + i - 1 <= n ; l++)
{
r = l + i - 1 ;
for(qwq int k = l ; k < r ; k++)
{
if(f[l][k] == f[k + 1][r] && f[l][k]) // 能够合并,也就是数目相同,并且需要保证这两个区间是能够合并的,也就是非 0 即可。
{
f[l][r] = std::max(f[l][r] , f[l][k] + 1) ;
ans = std::max(ans , f[l][r]) ;
}
}
}
}
printf("%lld\n" , ans) ;
return 0 ;
}
因为题目是英文的,所以这里给出大体意思:
就是有 \(n\) 个男生去寻找伴侣,每一个男生有自己固定的指数 \(d_i\),寻找伴侣的过程是有序的,假设第 \(i\) 个男生找伴侣时,他的愤怒值为 \((i - 1) \times d_i\) , 现在求解所有人的最小的总愤怒值。 \(n\leq 100 , d_i \leq 100\)
我们设 \(f_{i,j}\) 表示从第 \(i\) 个开始,到第 \(j\) 个结束,寻找伴侣的最小愤怒值。在状态转移的时候,我们还是枚举断点 \(k\) ,但是这里有些不同,我在这里卡了点时间。
首先我们是清楚的,对于 \(l,k\) 这个区间,\([k+ 1 ,r]\) 是需要等待的,也就是 \(cost(k+1 , r)\) 的怒气值,那么对于前面的区间来说 , 我们从 \([l ,k]\) 的小的区间来考虑,那么也就是 \([l+1 ,k]\) ,在上文中提到,区间 \(DP\) 我们可以将其理解是递归子序列对吧,那么我们在求解 \([l+1,k]\) 这一个序列完成后我们需要将 \(l\) 加上,那么 \(l\) 加上的代价是什么? 那必然是 \(l\) 一直等完这一个区间完成之后才行,所以我们需要加上 \(d_l \times (k - l)\)。同时我们考虑为什么后面的不加呢?实际上后面的已经算是加了,我们这里合并区间,对于 \(k\) 往后的已经是递归处理掉了,所以这里只需要考虑 \(l,k\) 这一个区间对其的影响就好。
如没有看明白,则请移居代码,因为笔者一开始想歪了,后来才转过来的,所以比较讲不是特别的清晰,内容中可能还含有之前的残余。
void clear() {
memset(f , 0 , sizeof(f)) ;
memset(d , 0 , sizeof(d)) ;
memset(sum , 0 , sizeof(sum)) ;
}
signed main() {
int T = read() ;
for(qwq int oi = 1 ; oi <= T ; oi++)
{
n = read() ; clear() ;
for(qwq int i = 1 ; i <= n ; i++)
d[i] = read() , sum[i] = sum[i - 1] + d[i] ;
for(qwq int len = 2 ; len <= n ; len++)
for(qwq int l = 1 ; l + len - 1 <= n ; l++)
{
int r = l + len - 1 ;
f[l][r] = inf ;
for(qwq int k = l ; k <= r ; k++)
f[l][r] = std::min(f[l][r] , f[l + 1][k] + f[k + 1][r] + d[l] * (k - l) + (sum[r] - sum[k]) * (k - l + 1)) ;
}
printf("Case #%lld: %lld\n" , oi , f[1][n]) ;
}
return 0 ;
}
先咕一下。
给定长度为 \(n\) 的序列 \(a\) ,求解按照规则取完这个序列的最大值。规则 : 假设当前取为第 \(k\) 次取,取的时候只能够从最前面或者最后面取,并且当前取的贡献为 \(a_i \times k\) , \(n\leq 2000 , a_i \leq 1000\)
非常简单的一个题,状态不必说为 \(f_{l,r}\) 为区间 \([l,r]\) 的最优解,但是我们发现贡献和当前取的次数有关,那么我们是否还需要再开一维维护第几次取?
不必,我们直接记忆化搜索即可,记忆化搜索可以直接让我们进入下一次进行乱搞。
状态转移方程 : \(f_{l,r,k} = max(f_{l+1,r,k+1}+k*a_l , f_{l,r-1,k+1} +k *a_r)\)
这里的 \(k\) 就是第几次取了,直接记忆化搜索里面维护就好了。
int n , a[kmaxn] ;
int f[kmaxn][kmaxn] ;
qaq int dp(int l , int r , int num) {
if(num == n + 1) return 0 ;
if(f[l][r]) return f[l][r] ;
int ret = std::max(dp(l + 1 , r , num + 1) + a[l] * num , dp(l , r - 1 , num + 1) + a[r] * num) ;
return f[l][r] = ret ;
}
signed main() {
n = read() ;
for(qwq int i = 1 ; i <= n ; i++) a[i] = read() ;
printf("%lld\n" , dp(1 , n , 1)) ;
return 0 ;
}
给定一个长度为 \(m\) 的字符串,希望通过增加或者删除某一个字符从而使得该字符串为回文字符串,但是增加或者删除某一个字符都会有一个贡献,现在求解最小贡献。
\(m \leq 2000\)
一开始想坑里去了
我们仍是设 \(f_{i,j}\) 表示将区间 \([i,j]\) 转变为回文字符串的最小贡献。我们考虑一下就是我们什么样的叫做回文字符串。是不是长度不为 \(1\) 的回文字符串必然关于其对称中心对称?
那么我们类比马拉车算法,枚举对称中心 ? 啥也能类比,我服我自己。。
那么我们如果找到 \(s_i = s_j\) 说明什么,这个这个区间的左右端点就是对称的,不管这个区间如何,那么我们显然是不是就不需要管这两个节点了,我们最后不都是要将其转变为对称嘛,我们只要动就会添加贡献,显然这时候什么都不做就是最优的,那么这个时候就是 \(f_{i,j} = f_{i+1,j-1}\) 。
那么我们找不到怎么办?我们分四种情况来搞一下就好。
int n , m ;
char s1[kmaxn] ;
int c[kmaxn][2] , s[kmaxn] , f[kmaxn][kmaxn];
signed main() {
n = read() , m = read() ;
std::cin >> s1 + 1 ;
for(qwq int i = 1 ; i <= m ; i++) s[i] = s1[i] - ‘0‘ ;
for(qwq int i = 1 ; i <= n ; i++)
{
char a ; std::cin >> a ;
c[a - ‘0‘][1] = read() ; // 加
c[a - ‘0‘][0] = read() ; // 删
}
// 发现一开始想了两种不可行的做法
for(qwq int len = 2 ; len <= m ; len++)
{
for(qwq int l = 1 , r ; l + len - 1 <= m ; l++ )
{
r = l + len - 1 ;
if(s[l] == s[r]) f[l][r] = f[l + 1][r - 1] ;
else
{
f[l][r] = inf ;
f[l][r] = std::min(f[l][r] , f[l][r - 1] + c[s[r]][1]) ; // 1
f[l][r] = std::min(f[l][r] , f[l + 1][r] + c[s[l]][0]) ; // 2
f[l][r] = std::min(f[l][r] , f[l + 1][r] + c[s[l]][1]) ; // 3
f[l][r] = std::min(f[l][r] , f[l][r - 1] + c[s[r]][0]) ; // 4
}
}
}
printf("%lld\n" , f[1][m]) ;
return 0 ;
}
给定一串序列,问有多少种初始序列经过如题操作可以得到此序列。
这里的状态稍微有所变化, \(f_{l,r,0|1}\) , \(f_{i,j,0}\) 表示第 \(i\) 个人从左边来的方案数,\(f_{i,j,1}\) 表示第 \(j\) 个人从右边来的方案数。
在转移的时候我们还是需要判断一下:
从左边进来肯定前1个人比他高,前 \(1\) 个人有 \(2\) 种情况,要么在 \(i+1\) 号位置,要么在 \(j\) 号位置。
从右边进来肯定前 \(1\) 个人比他矮,前 \(1\) 个人有 \(2\) 种情况,要么在 \(j-1\) 号位置,要么在 \(i\) 号位置。
状态转移涉及到一个判断,我以代码块的形式给出 :
if(a[l] < a[l + 1]) f[l][r][0] += f[l + 1][r][0] ;
if(a[l] < a[r]) f[l][r][0] += f[l + 1][r ][1] ;
if(a[l] < a[r]) f[l][r][1] += f[l][r - 1][0] ;
if(a[r] > a[r - 1]) f[l][r][1] += f[l][r - 1][1] ;
signed main()
{
n = read() ;
for(int i = 1 ; i <= n ; i++) a[i] = read() ;
for(int i = 1 ; i <= n ; i++) f[i][i][0] = 1 ;
for(int len = 1 ; len <= n ; len++)
{
for(int l = 1 ; l + len <= n ; l++)
{
int r = l + len ;
if(a[l] < a[l + 1]) f[l][r][0] += f[l + 1][r][0] ;
if(a[l] < a[r]) f[l][r][0] += f[l + 1][r ][1] ;
if(a[l] < a[r]) f[l][r][1] += f[l][r - 1][0] ;
if(a[r] > a[r - 1]) f[l][r][1] += f[l][r - 1][1] ;
f[l][r][0] %= kmod ;
f[l][r][1] %= kmod ;
}
}
printf("%lld\n" , (f[1][n][0] + f[1][n][1] ) %kmod ) ;
return 0 ;
}
这类问题因为某些特殊情况,从而导致我一下子取整个区间会更优秀,所以这时候我们就将整个区间取完,这个时候,我们就会发现,我们取完区间后,只可能在两个位置 ,左端点和右端点,那么我们一般将状态设为 \(f_{l,r,1|0}\) 表示我取完 \([l,r]\) 这一整个区间后,在右|左端点的最优解。
直接见题面,笔者这里不大会简化了。
我们这里根据上述的套路,我们就设状态为 \(f_{i,j,0|1}\) 表示我关闭掉 \([i,j]\) 这个区间在左右端点的最小耗电。那我们考虑一下状态转移。
状态转移方程直接看一下代码吧。
int f[kmaxn][kmaxn][2] , pos[kmaxn] , b[kmaxn] , sum[kmaxn];
signed main() {
n = read() , c = read() ;
for(qwq int i = 1 ; i <= n ; i++)
pos[i] = read() , b[i] = read() , sum[i] = sum[i - 1] + b[i] ;
for(qwq int i = 1 ; i <= n ; i++)
for(qwq int j = 1 ; j <= n ; j++)
f[i][j][0] = f[i][j][1] = inf ;
f[c][c][1] = f[c][c][0] = 0 ;
for(qwq int len = 1 ; len <= n ; len++)
{
for(qwq int l = 1 , r ; l + len - 1 <= n ; l++)
{
r = l + len - 1 ; if(l == r) continue ;
f[l][r][0] = std::min(f[l][r][0] , f[l + 1][r][0] + (pos[l + 1] - pos[l]) * (sum[l] + sum[n] - sum[r])) ;
f[l][r][0] = std::min(f[l][r][0] , f[l + 1][r][1] + (pos[r] - pos[l]) * (sum[l] + sum[n] - sum[r])) ;
f[l][r][1] = std::min(f[l][r][1] , f[l][r - 1][0] + (pos[r] - pos[l]) * (sum[l - 1] + sum[n] - sum[r - 1])) ;
f[l][r][1] = std::min(f[l][r][1] , f[l][r - 1][1] + (pos[r] - pos[r - 1]) * (sum[l - 1] + sum[n] - sum[r - 1])) ;
}
}
printf("%lld\n" , std::min(f[1][n][1] , f[1][n][0])) ;
return 0 ;
}
和关路灯差不多,建议直接看题面,这里不给出,因为是双倍经验。
这个题和关路灯就几乎一样的。就是加了一个离散化。
signed main() {
n = read() ;
for(qwq int i = 1 ; i <= n ; i++) pos[i] = read();
std::sort(pos + 1 , pos + 1 + n) ;
c = std::lower_bound(pos + 1 , pos + n + 1 , 0) - pos ;
for(qwq int i = n ; i >= c ; i--) pos[i + 1] = pos[i] ; // 加入 0 点,0以后的向后移动
memset(f , 63 , sizeof(f)) ;
f[c][c][0] = f[c][c][1] = 0 ; pos[c] = 0 ;
for(qwq int len = 2 ; len <= n + 1 ; len++)
{
for(qwq int l = 1 , r ; l + len - 1 <= n + 1 ; l++)
{
r = l + len - 1 ;
f[l][r][0] = std::min(f[l][r][0] , f[l + 1][r][0] + (pos[l + 1] - pos[l]) * (n - r + 1 + l)) ;
f[l][r][0] = std::min(f[l][r][0] , f[l + 1][r][1] + (pos[r] - pos[l]) * (n - r + 1 + l)) ;
f[l][r][1] = std::min(f[l][r][1] , f[l][r - 1][0] + (pos[r] - pos[l]) * (n - r + 1 + l)) ;
f[l][r][1] = std::min(f[l][r][1] , f[l][r - 1][1] + (pos[r] - pos[r - 1]) * (n - r + 1 + l)) ;
}
}
printf("%lld\n" , std::min(f[1][n + 1][1] , f[1][n + 1][0])) ;
return 0 ;
}
如有认为可以补充,则可以私信笔者,予以添加,感谢。
标签:http noi 还需要 避免 假设 大会 hnoi 四边形不等式 长度
原文地址:https://www.cnblogs.com/Zmonarch/p/14829326.html