题目大意
纯DP
- 这道题是动态规划+线段树的题,我们先给出超时的动态规划解法,再给出利用线段树的优化解法。
- 由于只是给了一个 n n n值,没给数列实际情况,因此我们按最坏情况打算,即最大值在第一个。
下面是动态规划操作。
- (1): 定义 d p [ i ] [ j ] dp[i][j] dp[i][j]: 在前 i i i个机器可选条件下,将最大数移动到第 j j j个位置所使用的最少机器数量。
- (2): 目标 d p [ m ] [ n ] dp[m][n] dp[m][n]: 即在所有机器可选条件下,将最大值移动到最后一位所需最少机器数量。
- (3): 状态转移方程式:
d p [ i ] [ j ] = { d p [ i − 1 ] [ j ] t [ i ] ! = j m i n { d p [ i − 1 ] [ j ] , m i n { d p [ i − 1 ] [ j ′ ] ∣ s i < = j ′ < = t i } } e l s e dp[i][j] = \begin{cases} dp[i-1][j] & t[i] !=j \\ min\{dp[i-1][j], min\{dp[i-1][j']|s_i <=j'<=t_i\}\} & else \end{cases} dp[i][j]={dp[i−1][j]min{dp[i−1][j],min{dp[i−1][j′]∣si<=j′<=ti}}t[i]!=jelse- s i , t i s_i, t_i si,ti:分别是第 i i i台机器负责的的起始和结束位置。
- 当
t
[
i
]
!
=
j
t[i] != j
t[i]!=j时: 如果最大值在第
i
i
i台机器负责的区间内,那么我们不能选择该机器,如果不在负责区间内,选择该机器也没用。因此我们舍弃该机器。
- 当 t [ i ] = j t[i] = j t[i]=j时: 我们可以选择或不选择该机器,若不选择,则为 d p [ i − 1 ] [ j ] dp[i-1][j] dp[i−1][j], 若选择,则需要前 i − 1 i-1 i−1个机器将最大值移到了 s [ i ] < = j < = t [ i ] s[i]<=j<=t[i] s[i]<=j<=t[i]之间。
- (4): 更新策略:由于 i i i递增,因此按照 i i i由小到大更新。
- (5): 初始化: d p [ 0 ] [ ∗ ] = i n f , d p [ 0 ] [ 1 ] = 0 dp[0][*] = inf, dp[0][1] = 0 dp[0][∗]=inf,dp[0][1]=0 , 我们假想最大值在第一个位置,因此不使用机器可以将最大值移动到第一个位置,而移动到其他位置都是不合法的。
- (6): 复杂度: O ( m n ) O(mn) O(mn)
- 超时DP代码:
#include<iostream>
using namespace std;
const int MAXN = 50005;
const int MAXM = 500005;
const int INF = 1<<20;
int dp[2][MAXN];
int s[MAXM], t[MAXM];
int main()
{
int n,m;
cin >> n >> m;
for(int i=1; i<=m; i++)
cin >> s[i] >> t[i];
for(int i=1; i<=n; i++)
{
dp[0][i] = INF;
}
dp[0][1] = 0;
for(int i=1; i<=m; i++)
{
for(int j=1; j<=n; j++)
{
if(t[i] != j)
dp[i%2][j] = dp[(i-1)%2][j];
else
{
dp[i%2][j] = dp[(i-1)%2][j];
for(int p=s[i]; p<=t[i]; p++)
{
dp[i%2][j] = min(dp[i%2][j], dp[(i-1)%2][p]+1);
}
}
}
}
cout << dp[m%2][n] << endl;
return 0;
}
线段树加速
线段树主要是快速进行单点更新,区间更新,单点查询,区间查询的数据结构,复杂度可以达到 O ( l o g n ) O(logn) O(logn),具体原理可以参看此博客。那么如何利用线段树加速上述的的DP呢?
- 再次观察状态转移方程式:
d p [ i ] [ j ] = { d p [ i − 1 ] [ j ] t [ i ] ! = j m i n { d p [ i − 1 ] [ j ] , m i n { d p [ i − 1 ] [ j ′ ] ∣ s i < = j ′ < = t i } } e l s e dp[i][j] = \begin{cases} dp[i-1][j] & t[i] !=j \\ min\{dp[i-1][j], min\{dp[i-1][j']|s_i <=j'<=t_i\}\} & else \end{cases} dp[i][j]={dp[i−1][j]min{dp[i−1][j],min{dp[i−1][j′]∣si<=j′<=ti}}t[i]!=jelse
对于一个新机器, 我们实际上只更新了j = t[i]的状态,其他保持不变, 也就是每次只进行了单点更新以及区间查询最小值。那么结合线段树,我们就可以把复杂度降为 O ( m l o g ( n ) ) O(mlog(n)) O(mlog(n)) - AC代码:
#include<iostream>
#include<stdio.h>
using namespace std;
const int MAX = 4 * 50000+5;
const int MAXM = 500000+5;
const int inf = 1 << 20;
int mathine_s[MAXM], mathine_e[MAXM];
struct Node
{
int s;
int e;
int data; // 维护最小值.
}dp[MAX];
void build(int s, int e, int cnt)
{
dp[cnt].s = s;
dp[cnt].e = e;
if(s == e-1)
{
if(s == 1) //相当于给dp[0][1]赋值0
dp[cnt].data = 0;
else
dp[cnt].data = inf;
return;
}
int mid = (s + e) >> 1;
int left = (cnt << 1) + 1;
int right = (cnt << 1) + 2;
build(s, mid, left);
build(mid, e, right);
dp[cnt].data = min(dp[left].data, dp[right].data);
return;
}
int query(int s, int e, int index)
{
if(s<=dp[index].s && dp[index].e <= e)
return dp[index].data;
else if(s >= dp[index].e || e <= dp[index].s)
return inf;
int left = (index << 1) + 1;
int right = (index << 1) + 2;
return min(query(s,e,left), query(s,e,right));
}
void update_one(int p, int key, int index)
{
if(p == dp[index].s && dp[index].s == dp[index].e-1)
{
dp[index].data = key;
return;
}
int mid = (dp[index].s + dp[index].e) >> 1;
if(p < mid)
update_one(p, key, (index<<1)+1);
else
update_one(p, key, (index<<1)+2);
dp[index].data = min(dp[(index<<1)+1].data, dp[(index<<1)+2].data);
return;
}
int main()
{
int n, m;
scanf("%d%d", &n, &m);
for(int i=1; i<=m; i++)
scanf("%d%d", &mathine_s[i], &mathine_e[i]);
build(0, n+1, 0);
for(int i=1; i<=m; i++)
{
int v = min(query(mathine_e[i], mathine_e[i]+1, 0), query(mathine_s[i], mathine_e[i]+1, 0) + 1);
update_one(mathine_e[i], v, 0);
}
int res = query(n, n+1, 0);
printf("%d\n", res);
return 0;
}