算法竞赛进阶指南0x51 线性DP

AcWing271. 杨老师的照相排列

image

思路

这是一个计数的题目,如果乱考虑,肯定会毫无头绪,所以我们从1号到最后一个依次进行安排。

经过反复实验,发现两个规律

  1. 每一行的同学必须是从左向右依次连续放置。(这样状态表示仅仅需要每一行的人数就行了)
  2. 下一行的人数不能多余上一行

有两种思考方式:

  1. lyd思考方式:
    现在已经安排好学生了,dp[a][b][c][d][e]是按照这种方法安排的总的方案数。然后往以后的情况推倒。
  2. y总思考方式:
    思考怎样才能推倒到现在的情况。

时间复杂度分析:

第一排,第二排……第五排的人数乘起来。
a + b + c + d + e 5 ≥ 5 a ∗ b ∗ c ∗ d ∗ e 当且仅当全部相等时才取等号 \frac{a+b+c+d+e}{5} \ge ^5\sqrt{a*b*c*d*e}\\ 当且仅当全部相等时才取等号 5a+b+c+d+e5abcde 当且仅当全部相等时才取等号

所以状态总数为 7 5 = 16807 7^5 = 16807 75=16807(因为还要从0开始计算!!!)

代码实现

版本一(这一个版本我不太好理解)

#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
ll dp[32][32][32][32][32];
ll N[8];
void solve(int n)
{
    memset(dp, 0, sizeof(dp));
    memset(N, 0, sizeof(N));
    for(int i = 1; i<= n; i++) scanf("%lld", N+i);
    dp[0][0][0][0][0] = 1;
    for(int a1 = 0; a1 <= N[1]; a1++)
        for(int a2 = 0; a2 <= N[2]; a2++)
            for(int a3 = 0; a3 <= N[3]; a3++)
                for(int a4 = 0; a4 <= N[4]; a4++)
                    for(int a5 = 0; a5 <= N[5]; a5++)
                    {
                        if(a1 < N[1]) dp[a1+1][a2][a3][a4][a5] += dp[a1][a2][a3][a4][a5];
                        if(a2<N[2] && a2 < a1) dp[a1][a2+1][a3][a4][a5] += dp[a1][a2][a3][a4][a5];
                        if(a3<N[3] && a3 < a2) dp[a1][a2][a3+1][a4][a5] += dp[a1][a2][a3][a4][a5];
                        if(a4<N[4] && a4 < a3) dp[a1][a2][a3][a4+1][a5] += dp[a1][a2][a3][a4][a5];
                        if(a5<N[5] && a5 < a4) dp[a1][a2][a3][a4][a5+1] += dp[a1][a2][a3][a4][a5];
                    }
    printf("%lld\n", dp[N[1]][N[2]][N[3]][N[4]][N[5]]);
}
int main()
{
    int n;
    while(scanf("%lld", &n) && n) solve(n);
    return 0;
}

版本二(y总版本+注释)

#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
ll dp[32][32][32][32][32];
ll N[8];
void solve(int n)
{
    memset(dp, 0, sizeof(dp));
    memset(N, 0, sizeof(N)); 
    for(int i = 1; i<= n; i++) scanf("%lld", N+i);
    dp[0][0][0][0][0] = 1;//以这种情况为初始情况的只有dp[1][0][0][0][0].
    for(ll a1 = 0; a1 <= N[1]; a1++)
        for(ll a2 = 0; a2 <= min(N[2], a1); a2++)//有一个取min,是只遍历合法的空间
            for(ll a3 = 0; a3 <= min(N[3], a2); a3++)
                for(ll a4 = 0; a4 <= min(N[4], a3); a4++)
                    for(ll a5 = 0; a5 <= min(N[5], a4); a5++)
                    {
                        ll &v = dp[a1][a2][a3][a4][a5];
                        if(a1 && a2<=a1-1) v+= dp[a1-1][a2][a3][a4][a5];//注意合理性检验
                        //当某一行不为0的时候,才可能是最后一个人是从这一行增加的
                        //在把某一行删除之后,要考虑所得到的集合是不是满足探索到的规律
                        if(a2 && a3<=a2-1) v+= dp[a1][a2-1][a3][a4][a5];
                        if(a3 && a4<=a3-1) v+= dp[a1][a2][a3-1][a4][a5];
                        if(a4 && a5<=a4-1) v+= dp[a1][a2][a3][a4-1][a5];
                        if(a5) v+= dp[a1][a2][a3][a4][a5-1];
                    }
    
    printf("%lld\n", dp[N[1]][N[2]][N[3]][N[4]][N[5]]);
}
int main()
{
    int n;
    while(scanf("%lld", &n), n) solve(n);
    return 0;
}

AcWing 272. 最长公共上升子序列

image

输入样例:

4
2 2 1 3
2 1 2 3

输出样例:

2

思路

状态表示

我们可以仿照最长公共子序列,设置状态f[i][j]来表示A[1~i]B[1~j]的最长上升子序列。但是由于上升子序列还需要知道最后一个值是怎么,所以不妨设状态
f[i][j]来表示A[1~i]B[1~j]的,以B[j]为结尾的最长公共上升子序列。

状态的属性是MAX(max可以重复求,但是一定不能遗漏)

状态转移

y总 认为:一定要考虑last进行划分!

初步划分为两种情况:包含A[i]这一个元素 和 不包含A[i]这一个元素。
对于第一种情况,显然是f[i-1][j]
对于第二种情况,就显得有一些麻烦(需要划分倒数第二个相同元素究竟还是哪一个)

for(int i = 1; i <= n; i++)
        for(int j = 1; j <= n; j++)
        {
            dp[i][j] = dp[i-1][j];//第一种情况
            int maxv = 1;//如果没有倒数第二个
            if(a[i] == b[j])//注意第二种情况是有条件的
            {
                for(int k = 1; k <= j-1; k++)
                {
                    if(b[k] < b[j])//这是第二种情况的第二个条件
                        maxv = max(maxv, dp[i-1][k]+1);
                }    
                dp[i][j] = max(dp[i][j], maxv);
            }
        }

y总可以AC,但我是超时

仔细考虑循环,现在是在第二层循环里面,所以i是不变,而a[i] == b[j],所以b[j]也是不变的。

同时,现在在第二层循环,a[i]是不变的。

所以第三层循环其实是已经知道答案了,还像苍蝇一样再遍历一遍。

代码实现

#include <bits/stdc++.h>
using namespace std;
#define  N 3020
int a[N], b[N];
int dp[N][N];
int main()
{
    int n;
    scanf("%d", &n);
    for(int i = 1; i <= n; i++) scanf("%d", a+i);
    for(int i = 1; i <= n; i++) scanf("%d", b+i);
    for(int i = 1; i <= n; i++)
    {
        int maxv = 1;
        for(int j = 1; j <= n; j++)
        {
            dp[i][j] = dp[i-1][j];
            if(a[i] == b[j])
            {
                dp[i][j] = max(dp[i][j], maxv);
                
            }
            if(b[j] < a[i]) maxv = max(maxv, dp[i][j]+1);//注意maxv的含义:第二种情况下的dp值
        }
    }
    int ans = 0;
    for(int i = 1; i<= n; i++)
        ans = max(ans, dp[n][i]);
    cout << ans;
    return 0;
}

AcWing273. 分级

image

输入样例:

7
1
3
2
4
5
3
9

输出样例:

3

思路

这一道题目我想了比较长的时间,理解的不够到位。

一、对B的元素总是在A中的理解

image

这个是y总的笔记。解释:

  1. A 1 , A 2 . . . A 5 A_1,A_2...A_5 A1,A2...A5表示的是原始的A数列。
  2. A 1 ′ , A 2 ′ . . . A 5 ′ A'_1,A'_2...A'_5 A1,A2...A5表示的是按照从小到大排序之后的A序列。
  3. B与哪一个A对齐就代表这一个B与这一个A是匹配的
  4. B的纵坐标代表了B与排好序的A序列中的元素值的大小对比。

现在要证明:总是存在一种最优解,使得在这组最优解里面,对于任意一个 x ∈ B x\in B xB,总有 x ∈ A x\in A xA。|
通俗一点讲,就是B是A的子集。

选取一个区间,(粉色框框里面)

假设这些 B i B_i Bi所对应的 A i A_i Ai的值 有x个值小于等于 A i ′ A'_i Ai,有 y个值大于等于 A i + 1 ′ A'_{i+1} Ai+1,分三种情况进行构造:

x > y ,说明如果将粉色框整体向下移动,直到最小的红点等于 ,此操作保证使整体结果更优。

x < y,则整体向上移动,使得最大的等于 ,可以使得总和变小。

x == y ,则上移下移都一样,总和不会改变。

引用自https://fangkaipeng.com/?p=11111

通过上面这种方法在三个点中消去一个点之后,再使用同样的方法想去剩余的两个点

问题:为什么不可以一个点一个点来进行考虑呢?
因为B序列是单调递增的。如果在上述的三个点中, B 2 ≥ A 2 ′ B_2\ge A'_2 B2A2,并且 B 3 ≤ A 1 ′ B_3\le A'_1 B3A1那么就如果单点考虑,那么我把 B 2 B_2 B2往上移动, B 3 B_3 B3往下移动,B序列就不再是非严格单调递增的了。

这样,B的取值就发生了离散,仅仅有A中拍好序的元素一样多。

二、闫氏动态规划的分析方法

可以有一个数组,f[i][j]表示所有从第一位到第i位,已经被匹配好,并且第i为是 A i ′ A'_i Ai的集合。

属性是sum的最小值

转移方程:

考虑上一位f[i-1][1],f[i-1][2],f[i-1][3].....f[i-1][j]

挑一个最小的 f[i][j] = min+abs(j-A[i])

代码实现

#include <bits/stdc++.h>
using namespace std;
#define N 2010
int a[N];
int n;
int b[N];
int dp[N][N];
int work()
{
    for(int i = 1; i<= n; i++) b[i] = a[i];
    sort(b+1, b+1+n);

    for(int i = 1; i<= n; i++)
    {
        int minv = 0x3f3f3f3f;
        for(int j = 1; j <= n; j++)
        {
            minv = min(minv, dp[i-1][j]);
            dp[i][j] = minv+abs(b[j]-a[i]);
        }
    }

    int ans = 0x3f3f3f3f;
    for(int i = 1; i <= n; i++)
    {
        ans = min(ans, dp[n][i]);
    }
    return ans;
}
int main()
{
    scanf("%d", &n);
    for(int i = 1; i <= n; i++) scanf("%d", a+i);

    int ans = work();

    reverse(a+1, a+1+n);//递减的反过来求一下就行了

    ans = min(ans, work());

    printf("%d", ans);
    return 0;
}

AcWing\274. 移动服务

一个公司有三个移动服务员,最初分别在位置 1,2,3处。如果某个位置(用一个整数表示)有一个请求,那么公司必须指派某名员工赶到那个地方去。某一时刻只有一个员工能移动,且不允许在同样的位置出现两个员工。从 pq 移动一个员工,需要花费 c(p,q)。这个函数不一定对称,但保证 c(p,p)=0。给出 N 个请求,请求发生的位置分别为 p 1∼p N。公司必须按顺序依次满足所有请求,且过程中不能去其他额外的位置,目标是最小化公司花费,请你帮忙计算这个最小花费。

输入格式

第 1行有两个整数 L,N,其中 L 是位置数量,N 是请求数量,每个位置从 1 到 L编号。
第 2 至 L+1 行每行包含 L 个非负整数,第 i+1 行的第 j 个数表示 c(i,j),并且它小于 2000。最后一行包含 N个整数,是请求列表。一开始三个服务员分别在位置 1,2,3。

输出格式

输出一个整数 M,表示最小花费。

数据范围

3≤L≤200,1≤N≤1000

输入样例:

5 9
0 1 1 1 1
1 0 2 3 2
1 1 0 4 1
2 1 5 0 1
4 2 3 4 0
4 2 4 1 5 4 3 2 1

输出样例:

5

思路

DP的本质就是暴力搜索的优化,所以不需要尽力去找特别优秀的状态标示

状态表示:f[i][x][y][z]i表示已经处理了第i个位置的请求。
x,y,z分别表示三个工作人员所在的位置。

但是很明显量太过于大!

容易联想到:当完成了某一个任务的时候,必定有一个人在第i个请求的发生地。

所以可以压缩到三个状态。

状态转移

前提:按照拓扑序来进行!!!

  1. 从某一个点扫描他会影响到的点(在这一个题目中,一个点仅仅会影响三个点)。
  2. 对于当前的点,从可以影响他的状态的点中找。

*问题:*在状态里面,x与y顺序可以交换吗?
不能,在这里,必须把工作人员编上号,而不能把他们视作三个相同的人。

我想用广度优先来减少遍历,可是代码太过于复杂

 tmp = q.front(), q.pop();
        int i = tmp.i;
        int z = p[i];
        int x = tmp.x;
        int y = tmp.y;
        if(i>=N) continue;
        if(p[i+1] != y && i+1 != z && y != z)
        {
            tmp.i = i+1, tmp.x = x, tmp.y = y;
            q.push(tmp);
            f[i+1][x][y] = min(f[i+1][x][y], f[i][x][y]+c[z][p[i+1]]);
        }

但是3的指数级别增长过快,所以还不如全部循环一遍。
同时,还要判断某一个数在不在队列里面。。。。

注意:如果遇到x,y,z在一起,那么就忽略。

代码实现

#include <bits/stdc++.h>
using namespace std;
#define N1 210
#define N2 1010
int c[N1][N1];
int f[N2][N1][N1];
int p[N2];
int main()
{
    int L, N;
    scanf("%d%d", &L, &N);
    for(int i = 1; i <= L; i++)
        for(int j = 1; j <= L; j++) scanf("%d", &c[i][j]);
    for(int i = 1; i <= N; i++) scanf("%d", p+i);
    memset(f, 0x3f, sizeof(f));

    f[0][1][2] = 0;//设置初始条件!
    p[0] = 3;

    for(int i =0; i <= N; i++)
        for(int x = 1; x <= L; x++)
            for(int y = 1; y <= L; y++)
            {
                int z = p[i];
                //这一个是要排除不可能的情况:
                //在下面的三种转移情况中,可能会出现两个快递员在一起的情况,虽然更新了,但是我在i++之后不考虑他就行了
                if(x==y || x == z || y == z) continue;

                //注意:在这里有一个大问题:
                f[i+1][x][y] = min(f[i+1][x][y], f[i][x][y]+c[z][p[i+1]]);
                f[i+1][y][z] = min(f[i+1][y][z], f[i][x][y]+c[x][p[i+1]]);//下一步还可以是f[i+1][z][y]
                //这种情况刚好与上述的情况是对称的,按道理来说,应该添加一遍,但是实际上,由于取的是min,
                //所以确保有一个就行了如果把语句换成
                //f[i+1][y][z] = min(f[i+1][y][z], f[i][x][y]+c[x][p[i+1]]),
                //最终结果就是在对称的位置上有一个同样大小的值
                f[i+1][x][z] = min(f[i+1][x][z], f[i][x][y]+c[y][p[i+1]]);
                
            }
    int ans = 0x3f3f3f3f;
    for(int i = 1; i <= L; i++)
        for(int j = 1; j <= L; j++)
            if(f[N][i][j] < ans && (p[N]!=i && p[N]!=j && i!=j)) ans = f[N][i][j];//注意排除不可能情况
    cout << ans;
    return 0;
}

AcWing275. 传纸条

小渊和小轩是好朋友也是同班同学,他们在一起总有谈不完的话题。一次素质拓展活动中,班上同学安排坐成一个 mn 列的矩阵,而小渊和小轩被安排在矩阵对角线的两端,因此,他们就无法直接交谈了。幸运的是,他们可以通过传纸条来进行交流。纸条要经由许多同学传到对方手里,小渊坐在矩阵的左上角,坐标 (1,1),小轩坐在矩阵的右下角,坐标 (m,n)。从小渊传到小轩的纸条只可以向下或者向右传递,从小轩传给小渊的纸条只可以向上或者向左传递。 在活动进行中,小渊希望给小轩传递一张纸条,同时希望小轩给他回复。班里每个同学都可以帮他们传递,但只会帮他们一次,也就是说如果此人在小渊递给小轩纸条的时候帮忙,那么在小轩递给小渊的时候就不会再帮忙,反之亦然。 还有一件事情需要注意,全班每个同学愿意帮忙的好感度有高有低(注意:小渊和小轩的好心程度没有定义,输入时用 0 表示),可以用一个 0∼100 的自然数来表示,数越大表示越好心。小渊和小轩希望尽可能找好心程度高的同学来帮忙传纸条,即找到来回两条传递路径,使得这两条路径上同学的好心程度之和最大。现在,请你帮助小渊和小轩找到这样的两条路径。

输入格式

第一行有 2 个用空格隔开的整数 mn,表示学生矩阵有 mn

接下来的 m 行是一个 m×n 的矩阵,矩阵中第 ij 列的整数表示坐在第 ij 列的学生的好心程度,每行的 n 个整数之间用空格隔开。

输出格式

输出一个整数,表示来回两条路上参与传递纸条的学生的好心程度之和的最大值。

数据范围

1≤n,m≤50

输入样例:

3 3
0 3 9
2 8 5
5 7 0

输出样例:

34

思路

在这道题目中,先后从左上到右下还有从右下到左上可以转化为两条从左上到右下的重合的路径。

在这道题目中,y总讲到了方格取数的问题,并且把这一道题目等价于方格取数,按照方格取数的方法来进行解答。最终答案是正确的。

证明过程参考 vlehr ,的题解2

方格数组的题目:假设方格里面是黄金(非负数),从左上角向右下角走,可以走两次,每一次向下或者是向右。到达一个格子以后,就把这一个格子的黄金全部拿走(黄金不可再生!)
询问:最多可以拿到多少的黄金。

image

两类情况:

  1. 当拓展出去的更优的时候,那么方格数数还有传纸条是一样的,肯定没有交点。
  2. 当拓展出去的和原来的一样的时候,方格数数还有传纸条的结果相同,但是过程不相同。

这道题目还有状态表示。

理论上是要有f[x1][y1][x2][y2]四种状态,
但是从左上方走到右下方,走且必须走 m + n m+n m+n步。

所以可以抽象f[k][x1][x2],这样,就可以把 N ∗ N ∗ N ∗ N N*N*N*N NNNN 转化为 ( N + N ) ∗ N ∗ N ∗ N (N+N)*N*N*N (N+N)NNN这种情况。

代码实现

#include <bits/stdc++.h>
using namespace std;
#define N 54
int f[N*2][N][N];
int c[N][N];
int main()
{
    int n, m;
    scanf("%d%d", &n, &m);//我的习惯是n行 m列
    for(int i = 1; i <= n; i++)
        for(int j = 1; j <= m; j++) 
            scanf("%d", &c[i][j]);
    for(int k = 2; k <= n+m; k++)
        for(int x1 = max(1, k-m); x1 <= min(k-1, n); x1++)
            for(int x2 = max(1, k-m); x2 <= min(k-1, n); x2++)
            {
                //if(x1 == x2 && x1!=1 && x1 != m) 这是我的一个BUG,注意,x1==x2!=1并不意味这一定是左上角的,因为可能是k!=2
                if(x1 == x2 && k!=2 && k != m+n)
                {
                    f[k][x1][x2] = -0x3f3f3f3f;
                    continue;//已经是不合法的情况,现在让其他点永远不可能从这一个点转移过去!
                }
                //和 y总 一样偷懒 ~ ~ ~
                //f[k][x1][x2] = 0;
                for(int a = 0; a <= 1; a++)
                    for(int b = 0; b <= 1; b++)
                    {
                        f[k][x1][x2] = max(f[k][x1][x2], f[k-1][x1-a][x2-b] + c[x1][k-x1]+c[x2][k-x2]);
                    }
            }
    cout << f[m+n][n][n];//这里是我出的BUG,一定要时刻注意我的状态表示具体代表的是什么

    return 0;
}

AcWing276. I-区域

N×M 的矩阵中,每个格子有一个权值,要求寻找一个包含 K 个格子的凸连通块(连通块中间没有空缺,并且轮廓是凸的),使这个连通块中的格子的权值和最大。

注意:凸连通块是指:连续的若干行,每行的左端点列号先递减、后递增,右端点列号先递增、后递减。

求出这个最大的权值和,并给出连通块的具体方案,输出任意一种方案即可。

输入格式

第一行包含三个整数 N,MK。接下来 N 行每行 M 个整数,表示 N×M 的矩阵上每个格子的权值(均不超过 1000)。

输出格式

第一行输出 Oil : X,其中 X 为最大权值和。

接下来 K 行每行两个整数 x**iy**i,用来描述所有格子的具体位置,每个格子位于第 x**i 行,第 y**i 列。

数据范围

1≤N,M≤15,0≤KN×M

输入样例:

2 3 4 
10 20 30 
40 2 3

输出样例:

Oil : 100 
1 1 
1 2 
1 3 
2 1

思路

这道题目真的真的是折磨折磨折大磨。———xjsc01

我选择放弃(调了一天,浪费一天,建议不要做)

错误代码

#include <bits/stdc++.h>
using namespace std;
#define N 16
int f[N][N*N][N][N][2][2];
struct STATE{
    int i, j, l, r, x, y;
}g[N][N*N][N][N][2][2];
int c[N][N];
int main()
{
    int n, m, k;
    scanf("%d%d%d", &n, &m, &k);
    for(int i = 1; i <= n; i++)
        for(int j = 1; j <= m; j ++)
            scanf("%d", &c[i][j]);
    for(int i = 1; i <= n; i++)
        for(int j = 0; j <= k; j++)//***************
            for(int l = 1; l <= m; l++)
                for(int r = l; r <= m; r++)
                {
                    if(r-l+1 > j) continue;//r-l+1 < k原来
                    //左扩张,有=右扩张
                    {
                        int &vf = f[i][j][l][r][1][0];
                        STATE &vg = g[i][j][l][r][1][0];
                        for(int p = l; p <= r; p++)
                            for(int q = p; q <= r; q++)
                            {
                                if(f[i-1][j-(r-l+1)][p][q][1][0] > vf)
                                {
                                    vf = f[i-1][j-(r-l+1)][p][q][1][0];
                                    vg.i = i-1, vg.j = j-(r-l+1), \
                                    vg.l = p, vg.r = q, vg.x = 1,vg.r = 0;
                                }
                            }
                        for(int xx = l; xx <= r; xx++) vf += c[i][xx];
                    }
                    //左扩张,右收缩
                    {
                        int &vf = f[i][j][l][r][1][1];
                        STATE &vg = g[i][j][l][r][1][1];
                        for(int p = l; p <= r; p++)
                            for(int q = r; q <= m; q++)
                                for(int y = 0; y <= 1; y++)
                                {
                                    if(f[i-1][j-(r-l+1)][p][q][1][y] > vf)
                                    {
                                        vf = f[i-1][j-(r-l+1)][p][q][1][y];
                                        vg.i = i-1, vg.j = j-(r-l+1), \
                                        vg.l = p, vg.r = q, vg.x = 1,vg.r = y;
                                    }
                                }
                        for(int xx = l; xx <= r; xx++) vf += c[i][xx];
                    }
                    //左收缩右扩张
                    {
                        int &vf = f[i][j][l][r][0][0];
                        STATE &vg = g[i][j][l][r][0][0];
                        for(int p = 1; p <= l; p++)
                            for(int q = l; q <= r; q++)
                                for(int x = 0; x <= 1; x++)
                                {
                                    if(f[i-1][j-(r-l+1)][p][q][x][0] > vf)
                                    {
                                        vf = f[i-1][j-(r-l+1)][p][q][x][0];
                                        vg.i = i-1, vg.j = j-(r-l+1), \
                                        vg.l = p, vg.r = q, vg.x = x,vg.r = 0;
                                    }
                                }
                        for(int xx = l; xx <= r; xx++) vf += c[i][xx];
                    }
                    //左收缩,右收缩
                    {
                        int &vf = f[i][j][l][r][0][1];
                        STATE &vg = g[i][j][l][r][0][1];
                        for(int p = 1; p <= l; p++)
                            for(int q = r; q <= m; q++)
                            for(int x = 0; x <= 1; x++)
                            for(int y = 0; y <= 1; y++)
                            {
                                if(f[i-1][j-(r-l+1)][p][q][x][y] >vf)
                                {
                                    vf = f[i-1][j-(r-l+1)][p][q][x][y];
                                    vg.i = i-1, vg.j = j-(r-l+1), \
                                    vg.l = p, vg.r = q, vg.x = x,vg.r = y;
                                }
                            }
                        for(int xx = l; xx <= r; xx++) vf += c[i][xx];
                    }
                }
    STATE state;
    int ans = 0;
    for(int i = 1; i <= n; i++)
        for(int l = 1; l <= m; l++)
            for(int r = l; r <= m; r++)
                for(int x = 0; x <= 1; x++)
                    for(int y = 0; y <= 1; y++)
                    {
                        if(f[i][k][l][r][x][y] > ans)
                        {
                            ans = f[i][k][l][r][x][y];
                            state.i = i, state.j = k;
                            state.l = l, state.r = r;
                            state.x = x, state.y = y;
                        }
                    }
    printf("Oil : %d\n", ans);
    while(state.j)
    {
        for(int xx = state.l; xx <= state.r; xx++)
            printf("%d %d\n", state.i, xx);
        state = g[state.i][state.j][state.l][state.r][state.x][state.y];
    }
    return 0;
}

AcWing277. 饼干

圣诞老人共有 M 个饼干,准备全部分给 N 个孩子。每个孩子有一个贪婪度,第 i 个孩子的贪婪度为 g[i]。

如果有 a[i] 个孩子拿到的饼干数比第 i 个孩子多,那么第 i 个孩子会产生 g[ia[i] 的怨气。

给定 NM 和序列 g,圣诞老人请你帮他安排一种分配方式,使得每个孩子至少分到一块饼干,并且所有孩子的怨气总和最小。

输入格式

第一行包含两个整数 N,M

第二行包含 N 个整数表示 g1∼g**N

输出格式

第一行一个整数表示最小怨气总和。第二行 N 个空格隔开的整数表示每个孩子分到的饼干数,若有多种方案,输出任意一种均可。

数据范围

1≤N≤30,NM≤5000,1≤g**i≤107

输入样例:

3 20
1 2 3

输出样例:

2
2 9 9

思路

**知识点一:**排序不等式

image
image

所以在本题中,最优解一定在给贪婪程度大的孩子分配多的糖果的情况里面。

DP状态:(精髓:由于所有孩子的嫉妒值仅仅与排名有关,如果要是小孩得到的糖果数量大于一,那么我可以全部减一,最终的答案并不会发生变化)

状态表示:f[i][j]表示在安排了经过排序的1~i个孩子,使用j个糖果,最终孩子的嫉妒值的最小值。

转态转移:(深邃的思想)
有两种情况(假定最右面为1 的个数)

  1. k==0,这个时候,给所有的孩子的糖果数全部减一。
  2. 1 ≤ k ≤ i 1 \le k \le i 1ki这个时候,相当于最后k个孩子有一个糖果,进而进行转移。

注意:上述转移可能来自不合法的情况,但是初始化是最大值,遍历过的一定是合法的。
不合的地方转移过来一定是无穷大,所以没有关系!

代码实现

#include <bits/stdc++.h>
using namespace std;
#define N1 32
#define N2 5010
typedef pair<int, int> PII;
PII a[N1];//first用来读入孩子的贪婪度,second用于确定开始的序号
int s[N1];//排序之后的贪婪度的前缀和
int f[N1][N2];
int ans[N1];
int main()
{
    int n, m;
    scanf("%d%d", &n, &m);
    for(int i = 1; i <= n; i++)
    {
        scanf("%d", &a[i].first);
        a[i].second = i;
    }

    sort(a+1, a+1+n);
    reverse(a+1, a+1+n);//偷懒做法

    memset(f, 0x3f, sizeof(f));//一定要进行初始化!~
    for(int i = 1; i <= n; i++) s[i] = s[i-1] + a[i].first;

    f[0][0] = 0;
    for(int i = 1; i <= n; i++)
        for(int j = i; j <= m; j++)//注意,我的i的其实条件就是为了使我每一次更新的全部是合法法。
        {
            f[i][j] = f[i][j-i];
            for(int k = 1; k <= i; k++)//注意:这一个状态的上一个转态可能是合法的,也可能不合法,
            //但是我每一次更新的全部都是合法法,不合法法没有被更新,所以是无穷大,并不会影响结果。
            {
                f[i][j] = min(f[i][j], f[i-k][j-k] + (s[i]-s[i-k])*(i-k));
            }   
        }
    
    printf("%d\n", f[n][m]);

    {//回溯!!!
        int i = n, j = m, h = 0;
        while(i)
        {
            if(f[i][j] == f[i][j-i])//如果是K== 0,即没有为 1 的方格数
            {
                h++;
                j -= i;
            }
            else
            {
                for(int k = 1; k <= i; k++)
                {
                    if(f[i][j] == f[i-k][j-k] + (s[i]-s[i-k])*(i-k))
                    {
                        for(int u = i-k+1; u <= i; u++)
                            ans[a[u].second] = 1+h;
                        i -= k;
                        j -= k;
                        break;
                    }
                }
            }
        }
    }
    for(int i = 1; i <= n; i++) printf("%d ", ans[i]);
    return 0;
}


  1. 引用自https://fangkaipeng.com/?p=1111 ↩︎

  2. 引用自https://www.acwing.com/solution/content/12389/ ↩︎

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值