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;
}
posted @ 2022-07-27 18:37  wzx_believer  阅读(165)  评论(0编辑  收藏  举报