dp专题
逆序dp
P1280 尼克的任务
链接:https://www.luogu.com.cn/problem/P1280
本题的关键在于顺推不好做 想着要倒推 想着要统计开始的时间点 排序是为了消除后效性!!!!是个非常好的模型
#include<iostream>
#include<algorithm>
using namespace std;
long int n,k,sum[10001],num=1,f[10001];
struct ren//结构体,一起排序 ,从大到小
{
long int ks,js;
};
ren z[10001];
int cmp(ren a,ren b)
{
return a.ks>b.ks;
}
int main()
{
long int i,j;
cin>>n>>k;
for(i=1;i<=k;i++)
{
cin>>z[i].ks>>z[i].js;
sum[z[i].ks]++;
}
sort(z+1,z+k+1,cmp);
for(i=n;i>=1;i--)//倒着搜
{
if(sum[i]==0)
f[i]=f[i+1]+1;
else for(j=1;j<=sum[i];j++)
{
if(f[i+z[num].js]>f[i])
f[i]=f[i+z[num].js];
num++;//当前已扫过的任务数
}
}
cout<<f[1]<<endl;
}
逆序dp
这个题目和费用提前算不太一样 两者本质上都是消除后效性
这个题不同的就是不知道后面到底哪些要选 而费用提前算是后面的一定都是会选的
所以这个题巧妙地从后往前转移 这样转移前面的一定是后面最优策略 转移方程非常像算期望值
#include <iostream>
#include <cstdio>
#include <algorithm>
using namespace std;
#define maxn 100005
double f[maxn];
double n,k,c,w;
int a[maxn],b[maxn];
int main()
{
int i;
scanf("%lf%lf%lf%lf",&n,&k,&c,&w);
for(i=1;i<=n;i++) scanf("%d%d",&a[i],&b[i]);
for(i=n;i>=1;i--)
{
if(a[i]==1) f[i]=max(f[i+1],f[i+1]*(1-k/100)+b[i]);
if(a[i]==2) f[i]=max(f[i+1],f[i+1]*(1+c/100)-b[i]);
}
printf("%.2lf",f[1]*w);
}
逆序dp
分析:
发现我们可以很随意的写出dp[i][j]表示初始点走到<i,j>需要的最小值
但是很快我们就会发现问题 跟新后面必须还得要当前路径权值和 对后面的转移会造成影响 也就是具有后效性 这样是没法维护的
我们考虑逆着来设计 dp[i][i]表示从<i,j>走到终点需要的最小值 转移的时候我们只需要知道从初始点走到<i,j>的最大路径和即可
class Solution {
public:
int calculateMinimumHP(vector<vector<int>>& dungeon) {
int n = dungeon.size(), m = dungeon[0].size();
vector<vector<int>> dp(n + 1, vector<int>(m + 1, INT_MAX));
dp[n][m - 1] = dp[n - 1][m] = 1;
for (int i = n - 1; i >= 0; --i) {
for (int j = m - 1; j >= 0; --j) {
int minn = min(dp[i + 1][j], dp[i][j + 1]);
dp[i][j] = max(minn - dungeon[i][j], 1);
}
}
return dp[0][0];
}
};
反向dp(结果推反推数据)
先考虑一个简单的问题:
代码很好写:
int f[5005], a[5005], n, m;
void slove() {
cin >> n >> m;
for (int i = 1; i <= n; i++)cin >> a[i];
f[0] = 1;
for (int i = 1; i <= n; i++) {
for (int j = m; j >= 0; j--) {
(f[a[i] + j] += f[j]) %= mod;
(f[a[i] / 2 + j] += f[j]) %= mod;
}
}
for (int i = 1; i <= m; i++)cout << f[i] << " ";
cout << endl;
}
重点是下一个问
分析:
由于这个dp相互都有关联,我们必须找到一个突破口。可以发现,当前所有f数组中,最小f[i]非0的i,只能是由一个西瓜的一半贡献而来的。
那我们只需要模仿这上面的dp,依次反向的减去这个i的贡献即可。
int f[5005];
int m;
vector<int>a;
void slove() {
cin >> m;
for (int i = 1; i <= m; i++)cin >> f[i];
f[0] = 1;
for (int i = 1; i <= m; i++) {
while (f[i]) {
a.push_back(2 * i);
for (int j = 0; j <= m; j++) {
f[i + j] -= f[j]; f[i + j] = (f[i + j] + mod) % mod;
f[2 * i + j] -= f[j]; f[2 * i + j] = (f[2 * i + j] + mod) % mod;
}
}
}
cout << a.size() << endl;
for (int x : a)cout << x << " ";
cout << endl;
}
反向dp(结果反推数据)
http://acm.hdu.edu.cn/showproblem.php?pid=6092
分析:
和上面那个题目是一样的
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
#define mem(s,t) memset(s,t,sizeof(s))
#define D(v) cout<<#v<<" "<<v<<endl
#define inf 0x3f3f3f3f
//#define LOCAL
inline void read(ll &x){
x=0;char p=getchar();
while(!(p<='9'&&p>='0'))p=getchar();
while(p<='9'&&p>='0')x*=10,x+=p-48,p=getchar();
}
const ll N =1e4+10;
ll B[N];
int main() {
#ifdef LOCAL
freopen("1006.in","r",stdin);
freopen("out.txt","w",stdout);
#endif
ll t;
read(t);
while(t--){
ll n,m;
read(n);read(m);
for(int i=0;i<=m;i++){
read(B[i]);
}
ll st=0,f=1;
for(ll x=0;x<n;x++){
for(ll i=1;i<=m;i++){
if(B[i]){
st=i;
break;
}
}
if(f) printf("%lld",st),f=0;//末尾空格处理
else printf(" %lld",st);
for(ll j=st;j<=m;j++){
B[j]-=B[j-st];
}
}
puts("");
}
return 0;
}
这个dp以前没见过 确实不会 怎么才能保证两两互不相交呢 在我印象里面没有这样操作过的dp
考虑换个想法 设dp[i,j,k]表示 前i个 两者差值为j 用了k次加倍 因为下标不能为负 所以整体向右偏移1300
初始化 dp[0,1300,0]=0 答案 max{dp[n,1300,i]} i属于[0,k]
转移方程:dp[i,j,k]=max(dp[i-1,j+v[i],k]+w[i],dp[i-1,j-v[i],k]+w[i],dp[i-1,j+2v[i],k-1]+w[i],dp[i-1,j-2v[i],k-1]+w[i])
#include<cstdio>
#include<cstring>
#include<algorithm>
#include<iostream>
#include<queue>
using namespace std;
const int N=103;
typedef long long ll;
ll f[N][3003][N],w[N],v[N];
int main()
{
memset(f,-0x3f,sizeof f);
f[0][1300][0]=0;
int n,m;
cin>>n>>m;
for(int i=1;i<=n;i++)
scanf("%lld%lld",&w[i],&v[i]);
for(int i=1;i<=n;i++)
for(int j=0;j<=m;j++)
for(int k=0;k<=2600;k++)
{
f[i][k][j]=f[i-1][k][j];
if(k>=2*v[i]&&j>=1)
f[i][k][j]=max(f[i][k][j],f[i-1][k-2*v[i]][j-1]+w[i]);
if(k>=v[i])
f[i][k][j]=max(f[i][k][j],f[i-1][k-v[i]][j]+w[i]);
if(k+2*v[i]<=2600&&j>=1)
f[i][k][j]=max(f[i][k][j],f[i-1][k+2*v[i]][j-1]+w[i]);
if(k+v[i]<=2600)
f[i][k][j]=max(f[i][k][j],f[i-1][k+v[i]][j]+w[i]);
}
ll ans=-0x3f3f3f3f;
for(int i=0;i<=m;i++)
ans=max(ans,f[n][1300][i]);
cout<<ans;
return 0;
}
https://www.luogu.org/problem/P2577
分析:
首先本题不是贪心排序就是dp
再数据范围<=200,就只有可能是dp了
考虑本题,容易想到应该尽量让那些打饭快却吃饭慢的排在前面
又因为所有人打饭的总时间是一定的,
即无论怎么安排所有人打完饭都会耗费这么多时间
所以我们只用考虑吃饭慢的排在前面则一定是最优的
但此时有两个窗口,怎样安排就要dp了
-
用f[i,j]记录前i个人排队,第一队用时为j的情况下最大用时。
-
不记录第2队状态的原因是可以由第一队状态推出来。
-
为了便于计算,建议使用前缀和维护一下,可以较为简易的计算出第2队的情况。
-
那么我们就得到了这样个方程:
加入第一队的情况下: f[i,j]=min(f[i,j],max(f[i-1,j-a[i].x],j+a[i].y));
当前最小时间为上一个人用的时间和这一个人用的时间的最大值
加入第二队同理
f[i,j]=max(f[i-1,j],a[i].y+b[i]-j)其中b[i]为前i个人排队所用的总时间
然而200*40000好像有点大,降维呗!
类似背包一样降维就行
code by std:
#include<bits/stdc++.h>
using namespace std;
struct lsg{int x,y;}a[1000];
int n,f[400001],sum,ans,b[1000];
bool pd(lsg x,lsg y){return x.y>y.y;}
int main(){
ios::sync_with_stdio(false);
cin>>n;
for (int i=1;i<=n;i++)cin>>a[i].x>>a[i].y;
sort(a+1,a+1+n,pd);memset(f,10,sizeof(f));f[0]=0;
for (int i=1;i<=n;i++)b[i]=a[i].x+b[i-1];
for (int i=1;i<=n;i++){
for (int j=sum;j>=0;j--){
f[j+a[i].x]=min(f[j+a[i].x],max(f[j],a[i].y+j+a[i].x));//将i加入第一个队列
f[j]=max(f[j],a[i].y+b[i]-j);//将i加入第二个队列
}
sum+=a[i].x;
}
ans=1e9;
for (int i=1;i<=sum;i++)ans=min(ans,f[i]);
cout<<ans<<endl;
}
https://www.luogu.org/problem/P2467
这是一道好题
题目描述
求1-n排列组成的波动数列的个数
因为要比较大小关系 所以dp里面最后数是多少是要记录的 又因为是每个数都不重复的排列
所以定义dp[i,j]表示用前 i 个数 最后一个数为 j 的方案数
dp[i,j]相当于dp[i-1,k]中原排列大于等于j的数都加1,再把j插到末尾后的新合法排列的方案数
答案有“M"型与"W"型,显然方案数是一样的,这里只考虑"W"型的,最后把答案*2就行了
用一个前缀和维护一下就好
这时你可能会有疑问,为什么偶数是枚举[1,j-1],而奇数是枚举[j,i-1],
因为只考虑“W”形态的,所以奇数一定是山峰的,而偶数一定山谷;
偶数就不用再每个加一了 直接加进去就好 奇数就要每个大于等于 j 的加一
所以奇数枚举的一定要比前一个位置上的数大,偶数枚举的一定要比前一个位置上的数小
#include<iostream>
#include<cstdio>
#include<cstring>
using namespace std;
#define N 4211
#define M(a) ((a)<=mod?(a):(a-mod))
inline int read(){
int x=0,f=1;
char c=getchar();
while(c<'0'||c>'9'){
if(c=='-')f=-1;
c=getchar();
}
while(c>='0'&&c<='9'){
x=(x<<3)+(x<<1)+c-'0';
c=getchar();
}
return x*f;
}
int n,mod;
int dp[N][N],ans=0;
int b[N];
int lowbit(int x){
return x&(-x);
}
void Add(int x,int d){
while(x<=n){
b[x]=M(b[x]+d);
x+=lowbit(x);
}
}
int Ask(int x){
int ans=0;
while(x){
ans=M(ans+b[x]);
x-=lowbit(x);
}
return ans;
}
int main(){
n=read(),mod=read();
for(int i=1;i<=n;i++){
dp[1][i]=1;
Add(i,1);
}
for(int i=2;i<=n;i++){
for(int j=1;j<=n;j++){
if(i&1){
if(i>j){
dp[i][j]=M(Ask(i-1)-Ask(j-1)+mod);
}
}
else{
dp[i][j]=Ask(j-1);
}
}
memset(b,0,sizeof(b));
for(int j=1;j<=n;j++){
Add(j,dp[i][j]);
}
}
for(int i=1;i<=n;i++){
ans=M(ans+dp[n][i]);
}
cout<<2*ans%mod<<endl;
return 0;
}
https://www.luogu.org/problem/P1005
分析:
发现啊,每一行怎么取数是互不干扰的,则,只用分别处理每一行就好
数据范围也在算法复杂度以内
很好联想到区间dp
dp[i,j]表示处理到该行,区间[i,j]的最优解
考虑怎么转移
只有从[i-1,j]和[i,j+1]转移过来(因为转移逐渐将区间缩小)
因为题目中说要取完,但是空区间是DP不出来的,然后就得手动模拟每个长度为1的区间
具体高精啊,区间dp啊,见代码了
#include <iostream>
#include <cstdio>
#include <algorithm>
#include <cstring>
#include <cmath>
using namespace std;
const int MAXN = 85, Mod = 10000; //高精四位压缩大法好
int n, m;
int ar[MAXN];
struct HP {
int p[505], len;
HP() {
memset(p, 0, sizeof p);
len = 0;
} //这是构造函数,用于直接创建一个高精度变量
void print() {
printf("%d", p[len]);
for (int i = len - 1; i > 0; i--) {
if (p[i] == 0) {
printf("0000");
continue;
}
for (int k = 10; k * p[i] < Mod; k *= 10)
printf("0");
printf("%d", p[i]);
}
} //四位压缩的输出
} f[MAXN][MAXN], base[MAXN], ans;
HP operator + (const HP &a, const HP &b) {
HP c; c.len = max(a.len, b.len); int x = 0;
for (int i = 1; i <= c.len; i++) {
c.p[i] = a.p[i] + b.p[i] + x;
x = c.p[i] / Mod;
c.p[i] %= Mod;
}
if (x > 0)
c.p[++c.len] = x;
return c;
} //高精+高精
HP operator * (const HP &a, const int &b) {
HP c; c.len = a.len; int x = 0;
for (int i = 1; i <= c.len; i++) {
c.p[i] = a.p[i] * b + x;
x = c.p[i] / Mod;
c.p[i] %= Mod;
}
while (x > 0)
c.p[++c.len] = x % Mod, x /= Mod;
return c;
} //高精*单精
HP max(const HP &a, const HP &b) {
if (a.len > b.len)
return a;
else if (a.len < b.len)
return b;
for (int i = a.len; i > 0; i--)
if (a.p[i] > b.p[i])
return a;
else if (a.p[i] < b.p[i])
return b;
return a;
} //比较取最大值
void BaseTwo() {
base[0].p[1] = 1, base[0].len = 1;
for (int i = 1; i <= m + 2; i++){
base[i] = base[i - 1] * 2;
}
} //预处理出2的幂
int main(void) {
scanf("%d%d", &n, &m);
BaseTwo();
while (n--) {
memset(f, 0, sizeof f);
for (int i = 1; i <= m; i++)
scanf("%d", &ar[i]);
for (int i = 1; i <= m; i++)
for (int j = m; j >= i; j--) { //因为终值是小区间,DP自然就从大区间开始
f[i][j] = max(f[i][j], f[i - 1][j] + base[m - j + i - 1] * ar[i - 1]);
f[i][j] = max(f[i][j], f[i][j + 1] + base[m - j + i - 1] * ar[j + 1]);
} //用结构体重载运算符写起来比较自然
HP Max;
for (int i = 1; i <= m; i++)
Max = max(Max, f[i][i] + base[m] * ar[i]);
ans = ans + Max; //记录到总答案中
}
ans.print(); //输出
return 0;
}
https://www.luogu.org/problem/P2511
分析
第一问,求最长的最短,很显然一个二分就行
那第二问计数
f[i,j]代表前i个数分成j块的方案数,
则f[i,j]=Σ f[k,j-1] (k>=left[i]&&k<i) ,而因为空间问题,是不可以开1000*50000个数组的,
考虑用到前缀和 因为[ , j ] 都是由 [ , j-1]转移过来的 直接降维 每次更新完f数组 最后更新前缀数组s
这是一道非常好的动规优化
时间复杂度(N*M)
#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
#include<queue>
#include<cmath>
using namespace std;
int read()
{
int x=0,f=1;
char ss=getchar();
while(ss<'0'||ss>'9'){if(ss=='-')f=-1;ss=getchar();}
while(ss>='0'&&ss<='9'){x=x*10+ss-'0';ss=getchar();}
return x*f;
}
const int mod=10007;
const int maxn=50010;
int n,m,mx,ans;
int a[maxn],sum[maxn];
int dp[maxn],S[maxn];
int rem[maxn];
int check(int x)
{
int tot=0,len=0;
for(int i=1;i<=n;++i)
{
if(len+a[i]>x) tot++,len=a[i];
else len+=a[i];
if(tot>m) return 0;
}
return tot<=m;
}
int DP(int x)
{
int k=0;
for(int i=1;i<=n;++i)
for(;k<i;++k)
if(sum[i]-sum[k]<=x){ rem[i]=k; break;}
int res=(sum[n]<=x);//因为后面是从2开始枚举的 特判是否有只能为1段的情况
for(int i=1;i<=n;++i)
{
if(sum[i]<=x) dp[i]=1;//初始化很重要 此时的dp[i] 表示前i个数分为1段的方案 如果没法分为1段的dp值就为0
S[i]=(S[i-1]+dp[i])%mod;
}
for(int i=2;i<=m+1;++i)
{
for(int j=1;j<=n;++j)
{
dp[j]=S[j-1];
//非常常见的一种方法 用于转移的时候数组下标可能为负 取0
if(rem[j]-1>=0) dp[j]=((dp[j]-S[rem[j]-1])%mod+mod)%mod;//注意减法出现负数
}
for(int j=1;j<=n;++j)
S[j]=(S[j-1]+dp[j])%mod;
res=(res+dp[n])%mod;
}
return res;
}
int main()
{
n=read();m=read();
for(int i=1;i<=n;++i)
a[i]=read(),sum[i]=sum[i-1]+a[i],mx=max(mx,a[i]);
int L=mx,R=sum[n],mid;
while(L<R)
{
mid=L+R>>1;
if(check(mid)) ans=mid,R=mid;
else L=mid+1;
}
printf("%d %d",ans,DP(ans));
return 0;
}
皇宫看守[树的最小点覆盖问题]
Description:
太平王世子事件后,陆小凤成了皇上特聘的御前一品侍卫。
皇宫以午门为起点,直到后宫嫔妃们的寝宫,呈一棵树的形状;某些宫殿间可以互相望见。大内保卫森严,三步一岗,五步一哨,每个宫殿都要有人全天候看守,在不同的宫殿安排看守所需的费用不同。
可是陆小凤手上的经费不足,无论如何也没法在每个宫殿都安置留守侍卫。
Input:
帮助陆小凤布置侍卫,在看守全部宫殿的前提下,使得花费的经费最少。
f[u][0]表示被父节点覆盖
f[u][1]表示被子节点覆盖
f[u][2]表示被自己覆盖
只有f[u][1]的转移麻烦一点
首先f[u][1]+=Σmin(f[v][1],f[v][2])
但是还得保证至少v中有一个点要选它自己的
所以记录d 表示子节点中min(f[v][2]-min(f[v][1],f[v][2]))
最后f[u][1]+=d 即可
#include<bits/stdc++.h>
using namespace std;
vector<int> s[1505];
int w[1505];
int f[1505][3];
bool v[1505];
void dp(int u){
// f[u][2]//self
// f[u][1]//son
// f[u][0]//fa
int y;
int d=0x7fffffff/2;
int sz=s[u].size();
for(int i=0;i<sz;i++){
dp(s[u][i]);
f[u][0]+=min(f[s[u][i]][2],f[s[u][i]][1]);
f[u][1]+=min(f[s[u][i]][2],f[s[u][i]][1]);
d=min(d,f[s[u][i]][2]-min(f[s[u][i]][2],f[s[u][i]][1]));
f[u][2]+=min(f[s[u][i]][2],min(f[s[u][i]][1],f[s[u][i]][0]));
}
f[u][1]+=d;
f[u][2]+=w[u];
}
int main(){
int n;
cin>>n;
int num,k,r,m;
for(int i=1;i<=n;i++){
cin>>num>>k>>m;
w[num]=k;
for(int j=1;j<=m;j++){
cin>>r;
v[r]=1;
s[num].push_back(r);
}
}
int root;
for(int i=1;i<=n;i++){
if(v[i]==0){
root=i;
break;
}
}
dp(root);
cout<<min(f[root][1],f[root][2]);
return 0;
}
https://codeforces.com/problemset/problem/1646/D
分析:
其实很好发现 如果两个点相邻的话一定是不会都是good点的(死循环)
所以变相就是求树的最大独立点集
但是这个题还要求点权之和最小 所以非good点我们都设为1 相应地 good点即为度数和
转移呢 就要在满足点集最大的条件下 去转移点权和最小
还比较特殊 需要输出每个点的点权 所以还需要一个dfs顺序推过去就好
注意:n=2的情况 相邻的点是能都成为good点的
#include <iostream>
#include <cstdio>
#include <vector>
using namespace std;
int read(){
int num=0, flag=1; char c=getchar();
while(!isdigit(c) && c!='-') c=getchar();
if(c == '-') c=getchar(), flag=-1;
while(isdigit(c)) num=num*10+c-'0', c=getchar();
return num*flag;
}
const int N = 290005;
int T, n;
vector<int> p[N];
int f[N][2], d[N][2], fa[N];
void dp(int x){
f[x][1] = 1, d[x][1]= p[x].size(); d[x][0]=1;
for(auto nex : p[x]){
if(nex == fa[x]) continue;
fa[nex] = x;
dp(nex);
f[x][1] += f[nex][0];
d[x][1] += d[nex][0];
if(f[nex][1] == f[nex][0]) f[x][0]+=f[nex][1], d[x][0]+=min(d[nex][1], d[nex][0]);
else if(f[nex][1] > f[nex][0]) f[x][0]+=f[nex][1], d[x][0]+=d[nex][1];
else f[x][0]+=f[nex][0], d[x][0]+=d[nex][0];
}
}
int a[N];
void build(int x, int flag){
if(flag){
a[x] = p[x].size();
for(auto i : p[x]) if(i!=fa[x]) build(i, 0);
}else{
a[x] = 1;
for(auto i : p[x]){
if(i == fa[x]) continue;
if(f[i][0] == f[i][1]){
if(d[i][0] < d[i][1]){
build(i, 0);
}else{
build(i, 1);
}
}else if(f[i][0] > f[i][1]){
build(i, 0);
}else{
build(i, 1);
}
}
}
}
int main(){
n = read();
for(int i=1; i<=n-1; i++){
int u=read(), v=read();
p[u].push_back(v);
p[v].push_back(u);
}
if(n == 2){
printf("%d %d\n%d %d", 2, 2, 1, 1);
return 0;
}
dp(1);
if(f[1][0] == f[1][1]){
if(d[1][0] < d[1][1]){
printf("%d %d\n", f[1][0], d[1][0]);
build(1, 0);
}else{
printf("%d %d\n", f[1][1], d[1][1]);
build(1, 1);
}
}else if(f[1][0] > f[1][1]) {
printf("%d %d\n", f[1][0], d[1][0]);
build(1, 0);
}else {
printf("%d %d\n", f[1][1], d[1][1]);
build(1, 1);
}
for(int i=1; i<=n; i++) printf("%d ", a[i]);
return 0;
}
https://www.luogu.com.cn/problem/P2216
题目描述
有一个 a×b 的整数组成的矩阵,现请你从中找出一个 n×n 的正方形区域,使得该区域所有数中的最大值和最小值的差最小。
分析:
相当于是二维的滑动窗口
具体怎么实现呢 进行两次的单调队列
第一次求出X[i][j] 和 x[i][j] 数组 分别表示 点(i,j) 向右n个长度 的最大值 和最小值 转移的时候用原数组转移
第二次求出Y[i][j] 和 y[i][j] 数组 分别表示 点(i,j) 为左上端点 长度为n 的矩形 的最大最小值 转移的时候用 X和x数组进行转移
#include <bits/stdc++.h>
using namespace std;
int n,m,k,front,FRONT,back,BACK,ans;
int a[1001][1001],q[1001],Q[1001];
int x[1001][1001],X[1001][1001];
int y[1001][1001],Y[1001][1001];
int main()
{
scanf("%d%d%d",&n,&m,&k);
for (int I=1;I<=n;I++)
for (int i=1;i<=m;i++)
scanf("%d",&a[I][i]);
for (int I=1;I<=n;I++)
{
FRONT=BACK=front=back=Q[1]=q[1]=1;
for (int i=2;i<=m;i++)
{
while (a[I][i]>=a[I][Q[BACK]]&&FRONT<=BACK) BACK--;
while (a[I][i]<=a[I][q[back]]&&front<=back) back--;
BACK++;back++;Q[BACK]=i;q[back]=i;
while (i-Q[FRONT]>=k) FRONT++;
while (i-q[front]>=k) front++;
if (i>=k) X[I][i-k+1]=a[I][Q[FRONT]],x[I][i-k+1]=a[I][q[front]];
}
}
for (int I=1;I<=m-k+1;I++)
{
FRONT=BACK=front=back=Q[1]=q[1]=1;
for (int i=2;i<=n;i++)
{
while (X[i][I]>=X[Q[BACK]][I]&&FRONT<=BACK) BACK--;
while (x[i][I]<=x[q[back]][I]&&front<=back) back--;
BACK++;back++;Q[BACK]=i;q[back]=i;
while (i-Q[FRONT]>=k) FRONT++;
while (i-q[front]>=k) front++;
if (i>=k) Y[i-k+1][I]=X[Q[FRONT]][I],y[i-k+1][I]=x[q[front]][I];
}
}
ans=0x3f3f3f3f;
for (int I=1;I<=n-k+1;I++)
for (int i=1;i<=m-k+1;i++)
ans=min(ans,Y[I][i]-y[I][i]);
printf("%d\n",ans);
return 0;
}
https://www.luogu.com.cn/problem/P2034
题意:
一个正整数序列 选择若干个数 使得和最大 不能选连续k个数
分析:
# include <algorithm>
# include <iostream>
# include <cstring>
# include <cstdio>
# include <queue>
# include <cmath>
# include <ctime>
# define R register
# define LL long long
using namespace std;
LL tot,d,n,k;
LL p[100010],head = 1,tail = 1;
LL q[100010],f[100010],ans;
inline void in(R LL &a){
R char c = getchar();R LL x=0,f=1;
while(!isdigit(c)){if(c == '-') f=-1; c =getchar();}
while(isdigit(c)) x = (x<<1)+(x<<3)+c-'0',c = getchar();
a = x*f;
}
inline void maxx(R LL &a,const LL b){a>b? 0:a=b;}
inline LL yg(){
// freopen("bronlily.in","r",stdin);
// freopen("bronlily.out","w",stdout);
in(n),in(k);
for(R int i=1; i<=n; ++i)
{
in(d);
tot += d;
f[i] = q[head]+1LL*d;
while(head<=tail&&q[tail]>=f[i]) tail--;
q[++tail] = f[i],p[tail] = i;
while(head<=tail&&p[head]<i-k) head++;
}
for(R int i=n-k; i<=n; ++i) maxx(ans,1LL*tot-1LL*f[i]);
printf("%lld",ans);
return 0;
}
LL youngsc = yg();
int main(){;}
分析:如果直接顺序递推肯定是不行的
考虑记忆化搜索 每个点能向四周拓展的最长的路径是一定的 当我在此访问该点的时候直接返回答案即可
# include <bits/stdc++.h>
using namespace std;
int n, m;
int g[110][110];
int dp[110][110];
int X[5] = {1, -1, 0, 0};
int Y[5] = {0, 0, 1, -1};
int find(int x, int y)
{
if (dp[x][y])
return dp[x][y];
dp[x][y] = 1;
for (int i=0; i<4; i++)
{
int x1 = X[i] + x;
int y1 = Y[i] + y;
if (x1<=0||y1<=0||x1>n||y1>m)
continue;
if (g[x][y] > g[x1][y1])
dp[x][y] = max(dp[x][y], find(x1, y1)+1);
}
return dp[x][y];
}
int main()
{
cin>>n>>m;
for (int i=1; i<=n; i++)
{
for (int j=1; j<=m; j++)
cin>>g[i][j];
}
int ans = 0;
for (int i=1; i<=n; i++)
{
for (int j=1; j<=m; j++)
{
// cout<<i<<" "<<j<<" "<<find(i, j)<<endl;
ans = max(ans, find(i, j));
}
}
cout<<ans<<endl;
return 0;
}
https://www.luogu.com.cn/problem/P2507
如果不存在a[i]!=b[i]的情况 只要对a数组和b数组排序然后依次对应就好 很好证明
现在考虑存在a[i]=b[i]的情况 最优解一定是相邻两个交换
很容易忽略另一种情况 i位置的a和前面交换 b和后面交换 !!!
#include<bits/stdc++.h>
using namespace std;
#define lowbit(x) x&(-x)
#define ll long long
#define Max 1e14
const int maxn=1e5+5;
int n;
ll dp[maxn];
ll a[maxn],b[maxn];
ll calc(int x,int y){
if(a[x]==b[y])return Max;
else return abs(a[x]-b[y]);
}
int main(){
cin>>n;
for(int i=1;i<=n;i++)scanf("%lld%lld",&a[i],&b[i]),dp[i]=Max;
sort(a+1,a+1+n);sort(b+1,b+1+n);
dp[0]=0;
for(int i=1;i<=n;i++){
if(i>=1)dp[i]=min(dp[i],dp[i-1]+calc(i,i));
if(i>=2)dp[i]=min(dp[i],dp[i-2]+calc(i-1,i)+calc(i,i-1));
if(i>=3){
dp[i]=min(dp[i],dp[i-3]+calc(i-1,i-2)+calc(i-2,i)+calc(i,i-1));
dp[i]=min(dp[i],dp[i-3]+calc(i-2,i-1)+calc(i,i-2)+calc(i-1,i));
}
}
if(dp[n]<Max)
cout<<dp[n];
else cout<<"-1";
return 0;
}
POJ 3042 区间DP(费用提前计算相关的DP)
n<=1000
分析:
很好想到朴素的区间dp
设dp[i][j][k][0]表示区间[i,j]已经走完用时k 最后在左端点的最小值
设dp[i][j][k][1]表示区间[i,j]已经走完用时k 最后在右端点的最小值
这样转移n三方的复杂度 但是数据范围n<=1000是不允许的
所以利用费用提前算 将k这一维给去掉 将贡献提前加上
dp[i][j][0]表示区间[i,j]已经走完 最后在左端点+后续贡献 的最小值
dp[i][j][1]表示区间[i,j]已经走完 最后在右端点+后续贡献 的最小值
#include <cstdio>
#include <algorithm>
using namespace std;
int n,l,rec,s[1005],f[1005][1005][2];
int main(){
scanf("%d%d",&n,&l);
for(int i=1;i<=n;i++)scanf("%d",&s[i]);
s[++n]=l;
sort(s+1,s+1+n);
for(int i=1;i<=n;i++)
if(s[i]==l)rec=l,f[i][i][0]=f[i][i][1]=0;
else f[i][i][0]=f[i][i][1]=0x3fffffff;
for(int i=rec;i;i--)
for(int j=i+1;j<=n;j++){
f[i][j][0]=min(f[i+1][j][0]+(s[i+1]-s[i])*(n-j+i),f[i+1][j][1]+(s[j]-s[i])*(n-j+i));
f[i][j][1]=min(f[i][j-1][1]+(s[j]-s[j-1])*(n-j+i),f[i][j-1][0]+(s[j]-s[i])*(n-j+i));
}
printf("%d\n",min(f[1][n][0],f[1][n][1]));
}
P4870 [BalticOI 2009 Day1] 甲虫
非常非常非常牛逼的一道题
分析:
很好想到费用提前算 每次走一段 剩下所有没有走的水滴贡献都减少
但是分析之后就发现 可能水滴可能会减少到负数 这样肯定是不能满足题意的
那要怎么办呢 看复杂度 三方是允许的
为了避免减到负数 我们就多开一维 记录最多接收到的水滴数
这样我们就能控制有效的水滴 使得不会减小到负数
这个操作真的牛逼了
还有一些细节问题都在代码里面
#include<bits/stdc++.h>
using namespace std;
#define ll long long
int start=0;
ll n,m;
ll a[305];
ll f[305][305][2];
#define INF 0x7fffffff
ll dp(int l)
{
memset(f,0x7f,sizeof(f));
f[start][start][0]=0;
f[start][start][1]=0;
for(int k=1;k<=l;k++)
{
for(int i=1;i<=n-k+1;i++)
{
int j=k+i-1;
f[i-1][j][0]=min(f[i-1][j][0],min(f[i][j][0]+(l-k)*(a[i]-a[i-1]),f[i][j][1]+(l-k)*(a[j]-a[i-1])));
f[i][j+1][1]=min(f[i][j+1][1],min(f[i][j][1]+(l-k)*(a[j+1]-a[j]),f[i][j][0]+(l-k)*(a[j+1]-a[i])));
}
}
ll minn=INF;
for(int i=1;i<=n-l+1;i++)
minn=min(minn,min(f[i][i+l-1][0],f[i][i+l-1][1]));
return minn;
}
int main()
{
cin>>n>>m;
bool zero=false;
for(int i=1;i<=n;i++)
{
scanf("%lld",&a[i]);
if(a[i]==0)zero=true;
}
if(zero==false)a[++n]=0;
sort(a+1,a+n+1);
for(int i=1;i<=n;i++)
if(a[i]==0)start=i;
ll maxn=-1;
for(int i=1;i<=n;i++)maxn=max(maxn,m*i-dp(i));
if(zero==false)maxn-=m;
cout<<maxn<<endl;
return 0;
}