21.8.28 dp

本文汇总了九个信息技术问题的解题思路,包括线性对齐、点的颜色组合代价、直线切割平面、动态规划、汉诺塔等,展示了从O(n^2)复杂度优化到O(nlogn)的解决方案。这些算法涉及数据结构、动态规划、图论等多个知识点,是算法工程师必备的技能集合。
摘要由CSDN通过智能技术生成

禁止转载

9.10 全部完工.

A O ( n 2 m ) O(n^2m) O(n2m).
B O ( n 2 ) O(n^2) O(n2).
C O ( n 2 ) O(n^2) O(n2).
D O ( n log ⁡ n ) O(n\log n) O(nlogn).
E O ( n 2 ) O(n^2) O(n2).
F O ( n log ⁡ n ) O(n\log n) O(nlogn).
G O ( n 2 ) O(n^2) O(n2).
H O ( n 3 k ) O(n^3k) O(n3k).
I O ( n ) O(n) O(n).
J O ( n ) O(n) O(n).
K O ( n log ⁡ n ) O(n\log n) O(nlogn).
L O ( ∑ i n i ) O(\sum\limits_{i}n_i) O(ini).
M O ( n log ⁡ n ) O(n\log n) O(nlogn).
N O ( n 2 ) O(n^2) O(n2).
O O ( n 2 ) O(n^2) O(n2).
P O ( n 2 ) O(n^2) O(n2).

这里是题解汇总


A[POJ 1923]

O ( n 2 m ) O(n^2m) O(n2m)被我水过去了我还是第一次见…

两条直线无非相交或者平行.

显然这个问题给直线编号等于白干,所以我们并不用关心直线的添加顺序.

我们的策略是将两两平行的直线合成一个组一起加进去.

设之前已经放了 i i i条直线,现在准备放 j j j条平行的线,根据上边定义这 j j j条直线都会和前 i i i条直线有交点.

于是一共会新增 i ∗ j i*j ij个交点.

再画个图发现,会多出 i ∗ j + 1 i*j+1 ij+1个新的区域.

于是我们 d p dp dp方程就写出来了.

由已知转移到未知好写一点,虽然反着来也行.

c o d e code code:

#include<iostream>
#include<cstdio>
#include<cstring>
using namespace std;
const int N = 110 , M = 10010;
int f[N][M] , n , m , Tcase;
int main()
{
    while(scanf("%d%d" , &n , &m) && (n + m))
    {
        for(int i = 1 ; i <= n ; i++)
            for(int j = 0 ; j <= m ; j++) f[i][j] = 0;
        for(int i = 1 ; i <= n ; i++) f[i][0] = i + 1;
        for(int i = 0 ; i <= n ; i++)
        {
            for(int k = 0 ; k <= m ; k++)
                if(f[i][k])
                    for(int j = 1 ; i + j <= n && i * j + k <= m ; j++)
                        f[i + j][k + j * i] = max(f[i + j][k + i * j] , f[i][k] + j * i + j);
        }
        if(f[n][m]) printf("Case %d: %d lines with exactly %d crossings can cut the plane into %d pieces at most.\n" , ++Tcase , n , m , f[n][m]);
        else printf("Case %d: %d lines cannot make exactly %d crossings.\n" , ++Tcase , n , m);
    }
    return 0;
}

B[POJ 2677]

这从左到右又从右到左不好做啊.

换个模型.

现在有若干点,给每个点标上黑或者白两种颜色

同种颜色 x x x最近的两个点会产生 d i s ( i , j ) dis(i,j) dis(i,j)的代价.

问最终总代价是多少.

其实这个方向没有关系…

那就好做了.

f [ i ] [ j ] f[i][j] f[i][j]表示黑点 x x x最大的点是 i i i,白点 x x x最大是 j j j.

转移直接枚举就行.

总时间复杂度 O ( n 3 ) O(n^3) O(n3),有点赌.

其实两类点没有本质上的区别,因为如果去的是黑,回的是白,白含了最右点,那就把黑白颠倒就行.

而且还有一个性质.

最终黑和白总有一个点转移的时候状态会出现 i − 1 i-1 i1.

假定 f [ i ] [ j ] f[i][j] f[i][j]中规定 i > j i>j i>j.

f [ i ] [ j ] f[i][j] f[i][j]只能由 f [ i − 1 ] [ k ]   ,   k ∈ [ 1 , i − 1 ] f[i-1][k]\ , \ k\in[1,i-1] f[i1][k] , k[1,i1]转移而来.

于是我们的转移是 O ( n ) O(n) O(n),总时间复杂度 O ( n 2 ) O(n^2) O(n2).

狗血的是,这份代码交 C + + C++ C++是对的,交 G + + G++ G++ W A WA WA.

c o d e code code:

#include<iostream>
#include<cstdio>
#include<algorithm>
#include<cmath>
using namespace std;
const int N = 3010;
const long double inf = 192413842272192472.5;
struct point
{
    long double x , y;
} a[N];
int n;
long double f[N][N] , ans;
bool cmp(point a , point b)
{
    return a.x < b.x;
}
long double dis(int i , int j)
{
    if(j == 0) return 0;
    long double x = a[i].x - a[j].x , y = a[i].y - a[j].y;
    return sqrt(x * x + y * y);
}
int main()
{
    while(scanf("%d" , &n) != EOF)
    {
        for(int i = 1 ; i <= n ; i++)
            for(int j = 1 ; j <= n ; j++) f[i][j] = inf;
        for(int i = 1 ; i <= n ; i++) scanf("%Lf%Lf" , &a[i].x , &a[i].y);
        if(n == 1)
        {
            printf("0\n");
            continue;
        }
        sort(a + 1 , a + n + 1 , cmp);
        f[2][1] = dis(2 , 1);
        for(int i = 3 ; i <= n ; i++)
        {
            for(int j = 1 ; j < i - 1 ; j++)
            {
                f[i][j] = min(f[i][j] , f[i - 1][j] + dis(i , i - 1));
                f[i][i - 1] = min(f[i][i - 1] , f[i - 1][j] + dis(i , j));
            }
        }
        for(int i = 1 ; i < n ; i++) f[n][n] = min(f[n][n] , f[n][i] + dis(n , i));
        printf("%.2Lf\n" , f[n][n]);
    }
    return 0;
}

C [POJ1239]

另类思想.

首先可以想到一个很自然的想法,设 d p [ i ] [ l ] [ r ] dp[i][l][r] dp[i][l][r]表示处理到第 i i i最后一个数由 [ l , r ] [l,r] [l,r]组成是否可行.

但与其这么设,还不如换个设法.

d p [ i ] dp[i] dp[i]表示就目前而言, [ d p [ i ] , i ] [dp[i],i] [dp[i],i]可以作为目前的最后一个数.

转移直接按位判断是否可行就行.

题目要求最后一个数尽量小,那我们 d p dp dp转移的时候取 max ⁡ \max max即可.

剩下字典序最大.

我们想到位数越多,字典序越大.

于是倒着再扫一遍,用类似上边的做法就行.

复杂度是 O ( n 2 ) O(n^2) O(n2).

c o d e code code:

#include<iostream>
#include<cstdio>
#include<cstring>
using namespace std;
const int N = 110;
int n , dpfor[N] , dpback[N];
char s[N];

bool cmp(int s1, int t1, int s2, int t2)
{
    while(s[s1] == '0' && s1 <= t1)
        s1++;
    while(s[s2] == '0' && s2 <= t2)
        s2++;
    if(s1 > t1 && s2 > t2)
        return true;
 
    if((t1 - s1) > (t2 - s2))
        return false;
    else if((t1 - s1) < (t2 - s2))
        return true;
    else
    {
        for(int i = s1, j = s2; i <= t1; i++, j++)
        {
            if(s[i] > s[j])
                return false;
            else if(s[i] < s[j])
                return true;
        }
    }
    return false;
}
int main()
{
    while(scanf("%s" , s + 1) != EOF)
    {
        n = strlen(s + 1);
        if(n == 1 && s[1] == '0') break;
        memset(dpfor , 0 , sizeof dpfor);
        memset(dpback , 0 , sizeof dpback);
        for(int i = 1 ; i <= n ; i++)
        {
            dpfor[i] = 1;
            for(int j = 1 ; j < i ; j++)
                if(cmp(dpfor[j] , j , j + 1 , i)) dpfor[i] = max(dpfor[i] , j + 1);
        }
        int tn = dpfor[n];
        dpback[tn] = n;
        for(int i = tn - 1 ; s[i] == '0' ; i--) dpback[i] = n;
        for(int i = tn - 1 ; i ; i--)
        {
            for(int j = i ; j <= tn - 1 ; j++)
            {
                if(cmp(i , j , j + 1 , dpback[j + 1]))
                    dpback[i] = max(dpback[i] , j);
            }
        }
        int pos = dpback[1] , i = 1;
        while(i <= n)
        {
            while(i <= pos && i <= n) printf("%c" , s[i++]);
            if(i <= n) printf(",");
            pos = dpback[pos + 1];
        }
        puts("");
    }
    return 0;
}

E [POJ 1958]

开始想的背包,因为转移简单.

后来找不到怎么设状态,就上网搜了一下…

只能说非常巧妙.

汉诺塔本质是,必定会把前一部分移到某一座塔,再移动剩下的.

并且两次移动的问题是独立的.

第一次移动就是我们的四塔移动,第二次相当于三塔移动.

于是设 f [ i ] f[i] f[i]表示用四塔移动 i i i个盘的最小步数, d [ i ] d[i] d[i]表示用三塔移动 i i i个盘的最小步数.

得到转移: f [ i ] = min ⁡ ( f [ j ] ∗ 2 + g [ i − j ] ) f[i]=\min(f[j]*2+g[i-j]) f[i]=min(f[j]2+g[ij]).

d [ i ] d[i] d[i]我们已经有了结论: d [ i ] = 2 i − 1 d[i]=2^i-1 d[i]=2i1,直接递推求即可.

时间复杂度 O ( n 2 ) O(n^2) O(n2).

c o d e code code:

#include<iostream>
#include<cstdio>
#include<cstring>
using namespace std;
const int N = 13;
int d[N] , f[N] , n;
int main()
{
    n = 12;
    d[0] = 1;
    for(int i = 1 ; i <= 12 ; i++) d[i] = d[i - 1] * 2;
    for(int i = 1 ; i <= 12 ; i++) d[i]--;
    memset(f , 0x3f , sizeof f);
    f[0] = 0;
    f[1] = 1 , f[2] = 3;
    for(int i = 3 ; i <= 12 ; i++)
        for(int j = 1 ; j < i ; j++)
            f[i] = min(f[i] , 2 * f[j] + d[i - j]);
    for(int i = 1 ; i <= 12 ; i++) cout << f[i] << endl;
    return 0;
}

G [CF 1551E]

首先是一个性质.

若当前删去第 i i i个数,则对 [ 1 , i − 1 ] [1,i-1] [1,i1]的个数没有影响.

d p [ i ] [ j ] dp[i][j] dp[i][j] i i i个数一共删了 j j j个数后,前 i i i个数最多能有几个符合.

转移枚举 j j j,总时间 O ( n 2 ) O(n^2) O(n2).

c o d e code code:

#include<bits/stdc++.h>
using namespace std;
const int N = 2010;
int T , a[N] , dp[N][N] , n , m;
int main()
{
    scanf("%d" , &T);
    while(T--)
    {
        scanf("%d%d" , &n , &m);
        for(int i = 1 ; i <= n ; i++) scanf("%d" , &a[i]);
        for(int i = 1 ; i <= n ; i++)
            for(int j = 0 ; j <= i ; j++)
            {
                dp[i][j] = max(dp[i][j] , dp[i - 1][j] + ((i - j) == a[i]));
                if(j) dp[i][j] = max(dp[i][j] , dp[i - 1][j - 1]);
            }
        int fl = 0;
        for(int i = 0 ; i <= n ; i++)
            if(dp[n][i] >= m)
            {
                printf("%d\n" , i);
                fl = 1;
                break;
            }
        if(!fl) printf("-1\n");
        for(int i = 0 ; i <= n ; i++) for(int j = 0 ; j <= n ; j++) dp[i][j] = 0;
    }
    
}

H [CF 1551F]

本来想着直接换根统计深度的,结果一直跑WA…

看了题解才发现…

同深度同子树选出来是不合法的…

所以我们需要的是正经的 d p dp dp.

我们设 d p [ i ] [ j ] dp[i][j] dp[i][j]表示 i i i这棵子树深度为 j j j的节点有多少个.

题目中 k k k相当于选一共 k k k棵子树统计答案.

选上 i i i这棵子树相当于乘上 d p [ i ] [ j ] dp[i][j] dp[i][j].

但是需要枚举 k k k,相当的麻烦.

考虑下怎么拆贡献?

发现这玩意不要求顺序.

又启示我们很神奇的东西.

我们将所有的 d p [ i ] [ j ] dp[i][j] dp[i][j]全部列出来,再设 f [ i ] [ j ] f[i][j] f[i][j]表示考虑了前 i i i棵子树,选出 j j j棵的方案数.

那对于当前枚举的子树,我们无非就是选或者不选.

转移是 O ( 1 ) O(1) O(1),枚举是 O ( n k ) O(nk) O(nk).

换根总时间复杂度是 O ( n 3 k ) O(n^3k) O(n3k),不过跑不满.

注意…再把根选上不满足两辆距离相等…犯傻逼了

c o d e code code:

#include<bits/stdc++.h>
using namespace std;
const int N = 110 , M = 210 , mod = 1e9 + 7;
int t , n , k , ans;
int dp[N][N] , f[N][N] , b[N] , len;
vector<int> ch[N];
void dfs(int x , int lst)
{
    dp[x][0] = 1;
    for(int i = 0 , siz = ch[x].size() ; i < siz ; i++)
    {
        int v = ch[x][i];
        if(v != lst)
        {
            dfs(v , x);
            for(int j = 1 ; j <= n ; j++)
                dp[x][j] += dp[v][j - 1];
        }
    }
}
int solve(int x , int dep)
{
    memset(b , 0 , sizeof b);
    len = 0;
    for(int i = 0 , siz = ch[x].size() ; i < siz ; i++)
    {
        int v = ch[x][i];
        if(dp[v][dep - 1]) b[++len] = dp[v][dep - 1];
    }
    f[1][0] = 1 ; f[1][1] = b[1];
    for(int i = 2 ; i <= len ; i++)
        for(int j = 0 ; j <= k ; j++)
        {
            f[i][j] = f[i - 1][j];
            if(j) f[i][j] = (f[i][j] + 1LL * f[i - 1][j - 1] * b[i] % mod) % mod;
        }
    return f[len][k];
}
int main()
{
    scanf("%d" , &t);
    while(t--)
    {
        scanf("%d%d" , &n , &k);
        for(int i = 1 , a , b ; i < n ; i++)
        {
            scanf("%d%d" , &a , &b);
            ch[a].push_back(b) ; ch[b].push_back(a);
        }
        if(k == 2)
        {
        	printf("%d\n" , 1LL * n * (n - 1) / 2 % mod);
        	for(int i = 1 ; i <= n ; i++) while(!ch[i].empty()) ch[i].pop_back();
        	continue;
		}
        ans = 0;
        for(int i = 1 ; i <= n ; i++)
        {
            memset(dp , 0 , sizeof dp);
            dfs(i , i);
            for(int j = 1 ; j <= n ; j++)
            {
            	memset(f , 0 , sizeof f);
                ans = (ans + solve(i , j)) % mod;
            }
                
        }
        printf("%d\n" , ans);
        for(int i = 1 ; i <= n ; i++) while(!ch[i].empty()) ch[i].pop_back();
    }
    return 0;
}

I [CF 1535C]

这…

f [ i ] [ 0 / 1 ] f[i][0/1] f[i][0/1]为处理完前 i i i位,以第 i i i位的 0 / 1 0/1 0/1结尾的子串的个数.

然而这样会算重,因为一个 ? ? ?对一个结尾最多只会产生一次贡献.

换个定义嘛, f [ i ] [ 0 / 1 ] f[i][0/1] f[i][0/1]表示处理完前 i i i位,以第 i i i位的 0 / 1 0/1 0/1结尾的子串最长长度.

这样统计的时候取个 max ⁡ \max max即可.

总时间复杂度 O ( n ) O(n) O(n).

c o d e code code:

#include<bits/stdc++.h>
#define ll long long
using namespace std;
const int N = 200010;
int n , t;
ll f[N][2] , ans;
char s[N];
int main()
{
    scanf("%d" , &t);
    while(t--)
    {
        scanf("%s" , s + 1);
        n = strlen(s + 1);
        for(int i = 1 ; i <= n ; i++)
        {
            if(s[i] == '?')
            {
                f[i][0] = f[i - 1][1] + 1;
                f[i][1] = f[i - 1][0] + 1;
            }
            else
            {
                if(s[i] == '1') f[i][0] = 0 , f[i][1] = f[i - 1][0] + 1;
                else f[i][1] = 0 , f[i][0] = f[i - 1][1] + 1;
            }
        }
        ans = 0;
        for(int i = 1 ; i <= n ; i++)
			ans = ans + max(f[i][0] , f[i][1]);
        printf("%lld\n" , ans);
        for(int i = 1 ; i <= n ; i++) f[i][0] = f[i][1] = 0;
    }
}

J [CF 1547E]

非常简单的 d p dp dp.

看到绝对值,肯定想办法搞掉.

搞掉还不简单,分两个方向转移就行了.

于是我们拆开了绝对值,接下来正式分析,以从左往右为例.

t i = min ⁡ 1 ≤ j ≤ k   & &   a j ≤ i ( t j + i − a j ) = i + min ⁡ 1 ≤ j ≤ k   & &   a j ≤ i ( t j − a j ) t_i=\min\limits_{1\le j\le k\ \&\& \ a_j\le i}(t_{j}+i-a_j)=i+ \min\limits_{1\le j\le k\ \&\& \ a_j\le i}(t_j-a_j) ti=1jk && ajimin(tj+iaj)=i+1jk && ajimin(tjaj).

直接维护一个最小值即可,有一处细节是有空调的地方也要转移…

总时间复杂度 O ( n ) O(n) O(n).

c o d e code code:

#include<bits/stdc++.h>
#define ll long long
using namespace std;
const int N = 300010;
int n , m , T , a[N] , vis[N];
ll t[N] , mmin;
int main()
{
    scanf("%d" , &T);
    while(T--)
    {
        scanf("%d%d" , &n , &m);
        for(int i = 1 ; i <= n ; i++) t[i] = 1e14 + 7 , vis[i] = 0;
        for(int i = 1 ; i <= m ; i++) scanf("%d" , &a[i]) , vis[a[i]] = 1;
        for(int i = 1 ; i <= m ; i++) scanf("%lld" , &t[a[i]]);
        mmin = 1e14 + 7;
        for(int i = 1 ; i <= n ; i++)
        {
            if(vis[i])
                mmin = min(mmin , t[i] - i);
            t[i] = min(t[i] , i + mmin);
        }
            
        mmin = 1e14 + 7;
        for(int i = n ; i >= 1 ; i--)
        {
            if(vis[i])
                mmin = min(mmin , t[i] + i);
            t[i] = min(t[i] , mmin - i);
        }
            
        for(int i = 1 ; i <= n ; i++) printf("%lld " , t[i]);
        puts("");
    }
    return 0;
}

K [CF1536 C]

我靠我是蠢比…

你这么想.

假设最后一个前缀被分成了若干段.

那由于每段比例都相等,那相加之后还是原来那个比例.

所以比例就是这个前缀的比例.

直接一个 m a p map map搞定.

c o d e code code:

#include<bits/stdc++.h>
using namespace std;
const int N = 500010;
char s[N];
int n , t;
int a , b;
map<pair<int , int> , int> st;
inline int gcd(int a , int b)
{
    while(b != 0)
    {
        int t = a ; a = b ; b = t % a;
    }
    return a;
}
int main()
{
    scanf("%d" , &t);
    while(t--)
    {
        scanf("%d" , &n);
        scanf("%s" , s + 1);
        st.clear();
        a = 0 , b = 0;
        for(int i = 1 ; i <= n ; i++)
        {
            if(s[i] == 'D') a++;
            else b++;
            int d = gcd(a , b);
            st[make_pair(a / d , b / d)]++;
            printf("%d " , st[make_pair(a / d , b / d)]);
        }
        puts("");
    }
    return 0;
}

L [CF 1529C]

结论猜的:每个点最后取值肯定是两个端点中的一个.

反正好写,写完跑了个样例发现跑对了.

那怎么证明呢?

我们发现当前点取值 x x x会产生的贡献来自两类,一类是比 x x x小的,一类是比 x x x大的.

考虑 x + 1 x+1 x+1,那么所有比 x x x小的数贡献 + 1 +1 +1,比 x x x大的数贡献 − 1 -1 1.

我们发现,若原本比 x x x小的数比比 x x x大的数要多,那么我们这么操作之后会使答案变大,反之亦然.

那我们只需要将对应操作尽量做多点就行了,等价于取区间端点.

由此考虑,设 f [ 0 / 1 ] [ x ] f[0/1][x] f[0/1][x]表示 x x x取了左/右端点的子树最大贡献.

直接做是 O ( 2 n ) O(2^n) O(2n)的,但我们发现最终每个点无非是两种取一个,那在子树合并的时候以按死根的取值为前提,再进行转移即可.

时间复杂度 O ( n ) O(n) O(n),总 O ( ∑ i n i ) O(\sum\limits_{i}n_i) O(ini).

c o d e code code:

#include<bits/stdc++.h>
#define ll long long
using namespace std;
const int N = 100010;
vector<int> ch[N];
int n , t , l[N] , r[N];
ll f[2][N];
ll Abs(int x){return x < 0 ? -x : x;}
void dfs(int x , int lst)
{
    for(int i = 0 , siz = ch[x].size() ; i < siz ; i++)
    {
        int v = ch[x][i];
        if(v == lst) continue;
        dfs(v , x);
        f[0][x] = f[0][x] + max(Abs(l[x] - l[v]) + f[0][v] , Abs(l[x] - r[v]) + f[1][v]);
        f[1][x] = f[1][x] + max(Abs(r[x] - l[v]) + f[0][v] , Abs(r[x] - r[v]) + f[1][v]);
    }
}
int main()
{
    scanf("%d" , &t);
    while(t--)
    {
        scanf("%d" , &n);
        for(int i = 1 ; i <= n ; i++) f[0][i] = f[1][i] = 0;
        for(int i = 1 ; i <= n ; i++) while(!ch[i].empty()) ch[i].pop_back();
        for(int i = 1 ; i <= n ; i++) scanf("%d%d" , &l[i] , &r[i]);
        for(int i = 1 , a , b ; i < n ; i++)
        {
            scanf("%d%d" , &a , &b);
            ch[a].push_back(b) ; ch[b].push_back(a);
        }
        dfs(1 , 1);
        printf("%lld\n" , max(f[0][1] , f[1][1]));
    }
    return 0;
}

M [CF 1529C]

我他吗方程早推出来了,额外代价就是找不到怎么处理.

f [ i ] f[i] f[i]表示一共 2 i 2i 2i个点的方案.

显然两种情况.

  1. 留一个所有大区间都包含的位置,相当于子状态 f [ 1 … i − 1 ] f[1\ldots i-1] f[1i1],加上就行,可以前缀和.
  2. 所有区间都是等长,这类贡献设为 g [ i ] g[i] g[i].

那最终就是 f [ i ] = s u m [ i − 1 ] + g [ i ] f[i]=sum[i-1]+g[i] f[i]=sum[i1]+g[i].

终点在于 g [ i ] g[i] g[i]怎么求.

设我们这一轮的长度为 k k k.

那么这个区间中间会出现若干个空,显然我们的左端点要落在这里面.

最右边的左端点会落在 k − 1 k-1 k1上.

于是整一个所用到的最右边的点为 2 k − 2 2k-2 2k2.

于是最终就等价于若干块长度为 2 k − 2 2k-2 2k2的整块拼成了 2 n 2n 2n,意味着 2 k − 2 2k-2 2k2还是 2 n 2n 2n的因数.

这个注意到 2 k − 2 2k-2 2k2 k k k一一映射,所以我们换成枚举 2 k − 2 2k-2 2k2,做这个东西可用埃氏筛.

主要时间开销在于 g [ i ] g[i] g[i],总时间为 O ( n log ⁡ n ) O(n\log n) O(nlogn).

c o d e code code:

#include<bits/stdc++.h>
using namespace std;
const int N = 1000010 , mod = 998244353;
int f[N] , sum[N] , g[N] , n;
void pre(int lmt)
{
    g[1] = 1 ;
    for(int i = 2 ; i <= lmt ; i++)
    {
        g[i] = 1LL * (g[i] + 2) % mod;
        // cout << g[i] << endl;
        for(int j = 2 ; i * j <= lmt ; j++)
            g[i * j]++;
    }
}
int main()
{
    scanf("%d" , &n);
    f[1] = 1;
    f[2] = 3;
    sum[1] = 1 ; sum[2] = 4;
    pre(n);
    for(int i = 3 ; i <= n ; i++)
    {
        f[i] = 1LL * (sum[i - 1] + g[i]) % mod;
        sum[i] = 1LL * (sum[i - 1] + f[i]) % mod;
    }
    printf("%d" , f[n]);
    return 0;
}

N [CF 1525D]

没头绪,硬着头皮写.

一个很重要的性质:存在一种最优方案使得最终黑点匹配的白点编号递增.

考虑设 f [ i ] [ j ] f[i][j] f[i][j]表示处理到第 i i i个黑点,第 i i i个黑点和 j j j匹配的最小代价.

转移 O ( n 2 ) O(n^2) O(n2),总时间 O ( n 3 ) O(n^3) O(n3),土块做法…

发现时间主要花在确定一个 j j j后的枚举 k k k的这个 O ( n ) O(n) O(n).

将方程列出来:

f [ i ] [ j ] = min ⁡ k < j ( f [ i − 1 ] [ k ] + ∣ i − j ∣ ) = ∣ i − j ∣ + min ⁡ k < j ( f [ i − 1 ] [ k ] ) f[i][j]=\min\limits_{k<j}(f[i-1][k]+|i-j|)=|i-j|+\min\limits_{k<j}(f[i-1][k]) f[i][j]=k<jmin(f[i1][k]+ij)=ij+k<jmin(f[i1][k]).

这下好办,记录前缀最小值即可.

时间复杂度 O ( n 2 ) O(n^2) O(n2).

c o d e code code:

#include<bits/stdc++.h>
using namespace std;
const int N = 5010;
int a[N] , b[N] , n , len;
int f[N][N] , mmin[N][N] , ans = 1e9 + 7;
int Abs(int x){return x < 0 ? -x : x;}
int main()
{
    scanf("%d" , &n);
    for(int i = 1 ; i <= n ; i++) scanf("%d" , &a[i]) , (a[i] == 1) ? b[++len] = i : 0;
    if(len == 0)
    {
        printf("0");
        return 0;
    }
    memset(f , 0x3f , sizeof f);
    memset(mmin , 0x3f , sizeof mmin);
    for(int i = 1 ; i <= n ; i++)
        if(a[i] == 0) f[1][i] = Abs(b[1] - i);
    for(int i = 1 ; i <= n ; i++) mmin[1][i] = min(f[1][i] , mmin[1][i - 1]);
    for(int i = 2 ; i <= len ; i++)
    {
        for(int j = 1 ; j <= n ; j++)
        {
            if(a[j]) continue;
            f[i][j] = Abs(b[i] - j) + mmin[i - 1][j - 1];
        }
        for(int j = 1 ; j <= n ; j++)
            mmin[i][j] = min(f[i][j] , mmin[i][j - 1]);
    }
    for(int i = 1 ; i <= n ; i++) ans = min(ans , f[len][i]);
    printf("%d" , ans);
    return 0;
}

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值