Day11:数位DP、状压DP、单调队列优化DP

经典题目:AccodersP2195 |【一本通提高数位动态规划】Amount of Degrees

题意: 求出区间 \([x,y]\) 中满足下面条件的所有的数:这个数 \(x\) 可以用 \(k\) 个不相等的 \(b\) 的整数幂之和。

首先这个区间是满足区间减法的。因此我们可以只考虑,\([0,y]\) 和 \([0,x-1]\) 这两个区间内的个数。

再考虑这些数的性质, 这些数转化成 \(b\) 进制以后,每一数位上的值都是 \(0\) 或 \(1\);

那么就可以在数位上下手。然后我们就可以建一棵完全二叉树,完全二叉树的节点上依次填写 \(0\) 和 \(1\),这样子,从根到叶子节点的路径就可以表示一个值。对于我们想要的数字,就是这条路径上的 \(1\) 的个数必须是 \(k\)个。对于各个子问题,我们可以预先处理处,到第 \(i\) 层有 \(j\) 个 \(1\)。这个就是一个简单的 \(dp\)。

然后对于 \(b\) 进制,我们可以把 \(n\) 转化成 \(b\) 进制之后,去找一个不超过 \(n\) 最大的满足条件的数,在把 \(b\) 进制直接当成 \(2\) 进制来做就可以。怎么去找这个‘最大’的数呢?就只在把 \(n\) 转化为 \(b\) 进制之后,从左边开始找到

第一个数位上不为 \(0\) 或 \(1\) 的位置,再把这个位置,和这个位置右边的所有数位上的值改为 \(1\) 。就可以了。

点击查看代码
#include <iostream>
#include <cstring>
#include <vector>
using namespace std; const int N = 35; int X, Y, K, B;
int C[N][N]; //C[n, m] n个里选m个 void init(int n) {
C[1][1] = C[1][0] = 1;
for (int i = 2; i <= n; i ++ ) {
C[i][0] = 1;
for (int j = 1; j <= i; j ++ ) C[i][j] = C[i - 1][j - 1] + C[i - 1][j];
}
} int dp(int n) {
if (!n) return 0; // 非法边界 vector<int> nums;
while(n) nums.push_back(n % B), n /= B; // 将数按位分解 int res = 0; // 问题答案
int last = 0; // 向下走时父亲的信息(此题是使用了多少个1)
for (int i = nums.size() - 1; i >= 0 ; i -- ) { // 最高位向最低位枚举
int x = nums[i];
if (x == 1) {
res += C[i][K - last];
last ++;
if(last > K) break;
}
else if (x > 0) {
res += C[i][K - last];
if (K - last - 1 >= 0) res += C[i][K - last - 1];
break;
} if(!i && last == K) res ++ ; } return res;
} int main() { scanf("%d%d%d%d", &X, &Y, &K, &B);
init(N - 1);
printf("%d\n", dp(Y) - dp(X - 1));
return 0;
}

洛谷P2657 [SCOI2009] windy 数

设f(x)表示x前面的数 即 \(t∈[1,x)\) 中 \(windy\) 数的个数

那么显然如果要求 \([l,r]\) 中 \(windy\) 数的个数就是:

\(F(l,r) = f(r+1)-f(l)\)

数位 \(dp\) 开始,预处理位数为 \(i\) 最高位为 \(j\) 的 \(windy\) 数个数 \(f[i][j]\)

转移:

\(f[i][j]=f[i-1][k]\) | 其中k是非负整数 \(k∈[0,9]\) 且 \(|k-j|>=2\)

初始值:

\(f[1][i]=1\) | 其中 \(i\) 为非负整数 \(i∈[0,9]\)

求 \(f(x)\) 怎么求呢?

为了方便处理先对数 \(x\) 进行按 \(10\) 进制位拆分到 \(a[]\) 数组

显然位数比 \(x\) 要小的数字都是合法的都在 \([1,x)\) 区间内,直接统计就行

位数和 \(x\) 一样最高位的数字比 \(x\) 小的数字都是合法的都在 \([1,x)\) 区间内直接统计就行

位数和 \(x\) 一样,最高位又和 \(x\) 一样我们从左到右扫一遍 \(x\) 各个位子上的数字大小然后枚举合法的该位子上的数 \([0,9]\) 判断是否合法就行。

点击查看代码
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
ll dp[15][15],ans;
int a[15],len;
long long L,R;
ll dfs(int pos,int pre,int st,int limit){
if(pos>len) return 1;
if(!limit&&dp[pos][pre]!=-1) return dp[pos][pre];
ll ret=0;
int res=limit?a[len-pos+1]:9;
for(int i=0; i<=res; i++){
if(abs(i-pre)<2) continue;
if(st&&i==0) ret+=dfs(pos+1,-2,1,limit&&i==res);
else ret+=dfs(pos+1,i,0,limit&&i==res);
}
if(!limit&&!st) dp[pos][pre]=ret;
return ret;
}
void part(ll x){
len=0;
while(x) a[++len]=x%10,x/=10;
memset(dp,-1,sizeof dp);
ans=dfs(1,-2,1,1);
}
int main(){
scanf("%lld%lld",&L,&R);
part(L-1);
ll minn=ans;
part(R);
ll maxx=ans;
printf("%lld",maxx-minn);
return 0;
}

单调队列优化DP

单调队列

  • 队列是单调的,递增或递减
  • 只能在队首或者队尾进行操作
  • 队列中维护所有在窗口中的元素,有一些元素是没用的,以区间最大值为例:

    • 所以从左到右尝试加入队列,弹出队尾比当前数更小的元素,弹出队首已经出窗口的元素,再队尾压入当前数
    • 这样,队首就是窗口最大值
  • 每个数只会弹入弹出一次,复杂度O(n)

点击查看单调队列模版
void getmax() {  // 和上图同理
int head = 0, tail = -1;
for (int i = 1; i <= k; i++) {
while (head <= tail) {
tail--; // 移动窗口(见下文)+优化队列
}
q[++tail] = i; // 入队
}
for (int i = k; i <= n; i++) {
while (head <= tail && a[q[tail]] <= a[i]) tail--; // 排除没用的值,优化队列内容。(详见上图)
cout << a[q[head]] << ""; // 输出最大值
}
}

经典例题:

题意

  • 给出一个长度为 \(n\) 的数组,编程输出每 \(k\) 个连续的数中的最大值和最小值。
  • \(n,k≤1e6\)
点击查看代码
#include <bits/stdc++.h>
using namespace std; int a[1001010];
deque <int> id;
deque <int> q;
int k, n;
int main() {
cin >> n >> k;
for (int i = 1; i <= n; i++) cin >> a[i]; for (int i = 1; i <= k; i++) {
while (!q.empty() and q.back() > a[i]) {
q.pop_back();
id.pop_back();
}
q.push_back (a[i]);
id.push_back (i);
}
for (int i = k + 1; i <= n + 1; i++) {
cout << q.front() << " ";
// cout << q.back() << endl;
if (id.front() < i - k + 1) {
id.pop_front();
q.pop_front();
}
while (!q.empty() and q.back() > a[i]) {
q.pop_back();
id.pop_back();
} q.push_back (a[i]);
id.push_back (i); }
for (int i = 1; i <= k; i++) {
while (!q.empty() and q.back() < a[i]) {
q.pop_back();
id.pop_back();
}
q.push_back (a[i]);
id.push_back (i);
}
cout<<endl;
for (int i = k + 1; i <= n + 1; i++) {
cout << q.front() << " ";
// cout << q.back() << endl;
if (id.front() < i - k + 1) {
id.pop_front();
q.pop_front();
}
while (!q.empty() and q.back() < a[i]) {
q.pop_back();
id.pop_back();
} q.push_back (a[i]);
id.push_back (i); }
}

最大连续和

  • 给你一个长度为n的整数序列。要求从中找一段连续长度不超过m的子序列,并且和最大
  • 求前缀和,转化成:
\[a[i]=max(sum(i)-sum(i-k)|k=1\dots m)
\]
  • 即:
\[a[i]=sum(i)-min(sum(i-k)|k=1\dots m)
\]
  • 滑动窗口求第二项即可
点击查看代码
int MaxSubSequence(const int A[], int N){
int ThisSum,MaxSum,i,j,k;
MaxSum = 0;
for(i=0;i<N;i++)
{
for(j=i;j<N;j++)
{
ThisSum = 0;
for(k=i;k<=j;k++)
{
ThisSum += A[k];
}
if(ThisSum > MaxSum)
MaxSum = ThisSum;
}
}
return MaxSum;
}

修建草坪

  • \(FJ\) 有 \(N\) (\(1 <= N <= 100,000\))只排成一排的奶牛。每只奶牛的效率是不同的,奶牛 \(i\) 的效率为 \(E_i\)。

    计算 \(FJ\) 选奶牛可以得到的最大效率,并且该方案中没有连续的超过 \(K\) 只奶牛。

  • 设 \(dp[i][0]\) 表示以i为结尾不选i的最大值, \(dp[i][1]\) 表示以 \(i\) 为结尾选 \(i\) 的最大值:

\[dp[i][0]=max(dp[i-1][0],dp[i-1][1])
\]
\[dp[i][1]=max(dp[j][0]+sum[i]-sum[j]|i-k\le j\le i-1)
\]

转化成

\[dp[i][1]=sum[i]+max(dp[j][0]-sum[j]|i-k\le j\le i-1)
\]

可以单调队列优化.

点击查看代码
#include <bits/stdc++.h>
typedef long long LL;
const int maxn = 100010;
LL dp[maxn], sum[maxn];
int que[maxn], E[maxn];
int head, tail;
LL max(LL a, LL b) {
return a > b? a : b;
}
void add(int j) {
while (head < tail && dp[j - 1] - sum[j] >= (que[tail - 1] > 0 ? dp[que[tail - 1] - 1] : 0) - sum[que[tail - 1]]) {
--tail;
}
que[tail++] = j;
}
void del(int j) {
if (head < tail && que[head] == j) {
++head;
}
}
int main() {
int n, k;
scanf("%d %d", &n, &k);
sum[0] = 0;
for (int i = 1; i <= n; ++i) {
scanf("%d", &E[i]);
sum[i] = sum[i - 1] + E[i];
}
dp[0] = 0;
que[tail++] = 0;
for (int i = 1; i <= n; ++i) {
add(i);
del(i - k - 1);
int j = que[head];
dp[i] = (j > 0 ? dp[j - 1] : 0) + sum[i] - sum[j];
}
LL ans = max(dp[n], dp[n - 1]);
printf("%lld\n", ans);
return 0;
}

旅行问题

  • \(John\) 打算驾驶一辆汽车周游一个环形公路。公路上总共有 \(n\) 个车站,每站都有若干升汽油(有的站可能油量为零),每升油可以让汽车行驶一千米。 \(John\) 必须从某个车站出发,一直按顺时针(或逆时针)方向走遍所有的车站,并回到起点。在一开始的时候,汽车内油量为零,\(John\) 每到一个车站就把该站所有的油都带上(起点站亦是如此),行驶过程中不能出现没有油的情况。
  • 判断以每个车站为起点能否按条件成功周游一周。

解法

  • 拆环成链,设每个加油站有 \(d[i]\) 油,到下一个加油站要 \(s[i]\) 千米
  • 那么从一个点出发,\(d[i]-s[i]\) 前缀和必须是非负数
  • 在 \(2n\) 的链上维护 \(n\) 的滑动窗口,求区间最小值,判断是不是负数
点击查看代码
#include <bits/stdc++.h>
#define int long long std::deque<int> q;
int n;
int b[2000001], a[2000001], ans[2000001]; signed main() {
scanf("%lld", &n);
for (int i = 1, x, y; i <= n; i++) {
scanf("%lld %lld", &x, &y);
a[i + n] = a[i] = x - y;
int d = n - i == 0 ? n : n - i;
b[d + n] = b[d] -= y;
b[2 * n - i + 1] = b[n - i + 1] += x;
}
for (int i = 2; i <= n * 2; i++)
a[i] += a[i - 1], b[i] += b[i - 1];
for (int i = 1; i < n * 2; i++) {
while (q.size() && i - q.front() + 1 > n)
q.pop_front();
while (q.size() && a[i] <= a[q.back()])
q.pop_back();
q.push_back(i);
if (i >= n) {
if (a[q.front()] - a[i - n] >= 0)
ans[i - n + 1] = 1;
}
}
q.clear();
for (int i = 1; i < n * 2; i++) {
while (q.size() && i - q.front() + 1 > n)
q.pop_front();
while (q.size() && b[i] <= b[q.back()])
q.pop_back();
q.push_back(i);
if (i >= n) {
if (b[q.front()] - b[i - n] >= 0)
ans[2 * n - i] = 1;
}
}
for (int i = 1; i <= n; i++)
if (ans[i])
printf("TAK\n");
else
printf("NIE\n");
}

BANK NOTEs

  • 一共有 \(n\) 种面值的硬币,面值分别为 \(b_1, b_2,..., b_n\). 但是每种硬币有 \(c_i\) 个,现在我们想要凑出面值 \(k\) 求最少要用多少个硬币.

  • \[n≤200,b_i,c_i,k≤20000
    \]

单调队列优化多重背包

不了解背包 \(DP\) 的请先阅读背包 \(DP\)。设 \(f_{i,j}\) 表示前 \(i\) 个物品装入承重为 \(j\) 的背包的最大价值,朴素的转移方程为

\[f_{i,j}=\max_{k=0}^{k_i}(f_{i-1,j-k\times w_i}+v_i\times k)
\]

时间复杂度 \(O(W\sum k_i)\)

考虑优化\(f_i\)的转移。为方便表述,设\(g_{x,y}=f_{i,x\times w_i+y}, g'_{x,y}=f_{i-1,x\times w_i+y}\),其中\(0\leq y<w_i\),则转移方程可以表示为:

\[g_{x,y}=\max_{k=0}^{k_i}(g'_{x-k,y}+v_i\times k)
\]

设\(G_{x,y}=g'_{x,y}-v_i\times x\)。则方程可以表示为:

\[g_{x,y}=\max_{k=0}^{k_i}(G_{x-k,y})+v_i\times x
\]

这样就转化为一个经典的单调队列优化形式了。\(G_{x,y}\) 可以 \(O(1)\) 计算,因此对于固定的 \(y\),我们可以在 \(O\left(\left[\frac{W}{w_i}\right]\right)\) 的时间内计算出 \(g_{x,y}\)。因此求出所有 \(g_{x,y}\) 的复杂度为

\[O\left(\left[\frac{W}{w_i}\right]\right)\times O(w_i)=O(W)。
\]

这样转移的总复杂度就降为 \(O(nW)\)。

点击查看代码
#include <bits/stdc++.h>
using namespace std;
const int N=205,M=20005;
int n,m,B[N],C[N],dp[M];
struct Data {
int Shuz,Weiz;
} Ddq[M];
int main() {
int i,j,k;
cin>>n;
for(i=1; i<=n; i++) cin>>B[i];
for(i=1; i<=n; i++) cin>>C[i];
cin>>m;
memset(dp,63,sizeof dp);
dp[0]=0;
for(i=1; i<=n; i++) {
for(j=0; j<B[i]; j++) {
int Head=1,Tail=0;
for(k=0;; k++) {
int x=k*B[i]+j;
if(x>m) break;
while(Head<Tail&&Ddq[Head].Weiz<k-C[i]) Head++;
while(Head<=Tail&&dp[x]-k<Ddq[Head].Shuz-Ddq[Head].Weiz) Tail--;
Ddq[++Tail]= {dp[x]-k,k};
dp[x]=min(dp[x],Ddq[Head].Shuz+k);
}
}
}
cout<<dp[m]<<endl;
return 0;
}

烽火传递

  • 在某两座城市之间有 \(n\) 个烽火台,每个烽火台发出信号都有一定的代价。为了使情报准确的传递,在连续 \(m\) 个烽火台中至少要有一个发出信号。现输入 \(n\) 、\(m\) 和每个烽火台发出的信号的代价,请计算总共最少需要话费多少代价,才能使敌军来袭之时,情报能在这两座城市之间准确的传递.
  • \[1<=m<=n<=1,000,000
    \]
  • 状态表示:\(f[i]\) 标识前 \(i\) 个烽火台并点燃第 \(i\) 个烽火台的最小合法代价.
  • 状态转移:\(f[i]=min(f[j]+w[i],i-m\le j\le i-1)\),最后扫描 \(m\) 个 \(f[i]\) 的值。
点击查看代码
#include <bits/stdc++.h>
using namespace std;
int dp[100010];
int a[100010];
int q[100010];
int main(){
int n,m;
scanf("%d%d",&n,&m);
int head=1;//表示队首的下标。
int tail=1;//表示队尾的下标。
for(int i=1;i<=n;i++){
scanf("%d",&a[i]);
}
for(int i=1;i<=n;i++){
dp[i]=dp[q[head]]+a[i];
while(tail>=head&&dp[i]<=dp[q[tail]])//每次更新都表示弹出队尾元素。
tail--;
q[++tail]=i;//弹完后,我们保证这个队列是单调的且新加入的元素一定是队尾元素。
while(q[head]<i+1-m)//判断队首元素是否合法,如果不合法,将其弹掉。
head++;
}
printf("%d",dp[q[head]]);//这一步比较经典。我们维护的单调队列中,队首元素一定是合法的(在最后的m个烽火台之内),
//所以我们选择这其中的最小者更新即可
return 0;
}

2025dsfz集训Day11:数位DP、状态压缩DP、单调队列优化DP的更多相关文章

  1. [小明打联盟][斜率/单调队列 优化dp][背包]

    链接:https://ac.nowcoder.com/acm/problem/14553来源:牛客网 题目描述 小明很喜欢打游戏,现在已知一个新英雄即将推出,他同样拥有四个技能,其中三个小技能的释放时 ...

  2. hdu3401:单调队列优化dp

    第一个单调队列优化dp 写了半天,最后初始化搞错了还一直wa.. 题目大意: 炒股,总共 t 天,每天可以买入na[i]股,卖出nb[i]股,价钱分别为pa[i]和pb[i],最大同时拥有p股 且一次 ...

  3. Parade(单调队列优化dp)

    题目连接:http://acm.hdu.edu.cn/showproblem.php?pid=2490 Parade Time Limit: 4000/2000 MS (Java/Others)    ...

  4. 【单调队列优化dp】 分组

    [单调队列优化dp] 分组 >>>>题目 [题目] 给定一行n个非负整数,现在你可以选择其中若干个数,但不能有连续k个数被选择.你的任务是使得选出的数字的和最大 [输入格式] ...

  5. 2018.09.10 bzoj1855: [Scoi2010]股票交易(单调队列优化dp)

    传送门 单调队列优化dp好题. 有一个很明显的状态设置是f[i][j]表示前i天完剩下了j分股票的最优值. 显然f[i][j]可以从f[i-w-1][k]转移过来. 方程很好推啊. 对于j<kj ...

  6. BZOJ 1499 [NOI2005] 瑰丽华尔兹 | 单调队列优化DP

    BZOJ 1499 瑰丽华尔兹 | 单调队列优化DP 题意 有一块\(n \times m\)的矩形地面,上面有一些障碍(用'#'表示),其余的是空地(用'.'表示).每时每刻,地面都会向某个方向倾斜 ...

  7. 单调队列优化dp

    洛谷p3800(单调队列优化DP) 题目背景 据说在红雾异变时,博丽灵梦单身前往红魔馆,用十分强硬的手段将事件解决了. 然而当时灵梦在Power达到MAX之前,不具有“上线收点”的能力,所以她想要知道 ...

  8. SCOI 股票交易 单调队列优化dp

    这道题 我很蒙.....首先依照搞单调队列优化dp的一般思路 先写出状态转移方程 在想法子去优化 这个题目中说道w就是这一天要是进行操作就是从前w-1天转移而来因为之前的w天不允许有操作!就是与这些天 ...

  9. bzoj1855: [Scoi2010]股票交易 单调队列优化dp ||HDU 3401

    这道题就是典型的单调队列优化dp了 很明显状态转移的方式有三种 1.前一天不买不卖: dp[i][j]=max(dp[i-1][j],dp[i][j]) 2.前i-W-1天买进一些股: dp[i][j ...

  10. Mice and Holes 单调队列优化dp

    Mice and Holes 单调队列优化dp n个老鼠,m个洞,告诉你他们的一维坐标和m个洞的容量限制,问最小总距离.1 ≤ n, m ≤ 5000. ​ 首先列出朴素的dp方程:\(f[i][j] ...

随机推荐

  1. 玩转云端 | AccessOne实用窍门之三步搞定门户网站防护与加速

    随着互联网的飞速发展,网站建设已成为企事业单位推广.提供服务的重要途径之一.在数字技术快速迭代的当下,如何在保障网站安全的前提下提供高效服务,是企事业单位需要着重考虑的内容. 网站安全防护是网站建设后 ...

  2. 如何正确配置 .gitignore 以忽略特定文件夹下的文件(除指定子文件夹外)

    在使用 Git 进行版本控制时,.gitignore 文件是一个非常有用的工具,可以帮助我们忽略不需要跟踪的文件或文件夹.然而,有时我们需要忽略某个文件夹下的所有内容,但保留其中的某个子文件夹.本文将 ...

  3. Asp.Net Core3.0 微信小程序统一下单

    微信统一下单开发文档:https://pay.weixin.qq.com/wiki/doc/api/native.php?chapter=9_1 微信支付小程序支付文档:https://pay.wei ...

  4. 关于 False、True、0、1、tinyint(1) 的说明

    MySQL 保存 Boolean 值时,用 1 代表 TRUE,0 代表 FALSE:类似一个 bit 位,默认没有数据,即为 0,也即 Faslse MySQL 存储 Boolean 值的类型为 t ...

  5. HTTPS 与 HTTP 的区别在哪?

    HTTP与HTTPS作为互联网数据传输的核心协议,其通信机制与安全特性深刻影响着现代网络应用的可靠性与用户体验.本文将解析两者的通信流程.安全机制及核心差异. 一.HTTP的通信机制 先来看看HTTP ...

  6. 关于我在使用Steamlit中碰到的问题及解决方案总结

    Steamlit 并不支持一个可以预览本地文件的路径选择器(并不上传文件) 解决方案:使用 Python 自带的 tkinter 来完成 参考:[Streamlit 选择文件夹的曲折方案]Stream ...

  7. 分享4款.NET开源、免费、实用的商城系统

    前言 今天大姚给大家分享4款.NET开源.免费.实用的商城系统,希望可以帮助到有商城系统开发需求的同学. nopCommerce nopCommerce是一个.NET开源功能丰富.免费.灵活且可定制的 ...

  8. vue watch监听路由变化

    vue watch监听路由变化 // 监听 this.$route.path // watch监听非DOM元素的改变 watch:{ '$route.path':function(to,from){ ...

  9. 什么是VMware vSphere

    VMware vSphere不是特定的产品或软件.VMware vSphere是整个VMware套件的商业名称.VMware vSphere堆栈包括虚拟化,管理和界面层.VMware vSphere的 ...

  10. 鸿蒙NEXT开发案例:程序员计算器

    [环境准备] • 操作系统:Windows 10 • 开发工具:DevEco Studio 5.0.1 Release Build Version: 5.0.5.306 • 目标设备:华为Mate60 ...