2019年电子科技大学ACM暑期前集训动态规划专题解题报告

https://acm.uestc.edu.cn/problem/oyhuan-you-shi-jie

这题是一个遍历所有节点的最小总距离问题。首先把每两个节点之间的距离存入一个矩阵(虽然好像并不能节省多少时间)。由于是无向图距离,可以用下三角矩阵。求解方法是状压dp,状态定义为当前位置和到过的点。可以用一个数(二进制)表示到过的点,多次循环,对于每一个状态,找到所有可以从其他状态一步进入该状态的路径(也就是枚举任意两个不同的到达过的点),选出其中总权最小的一条路径。根据二进制大小的特点,直接从1(只到过1号位置)枚举到$(1«n)-1$(到过所有n个位置)即可保证求值的顺序正确。枚举循环结束之后,在到过所有点的状态中找出最小总权,即为所求答案

代码及注释:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
#include <bits/stdc++.h>
using namespace std;
using ll = long long;
int n, maps[17][2];
ll dp[17][1 << 17];
ll path[17 * 18 / 2];
inline int tri(int m, int n) {//计算三角矩阵下标
  if (m < n) swap(m, n);
  return m * (m + 1) / 2 + n;
}
int main() {
  ios::sync_with_stdio(0);
  cin.tie(0);
  int s;
  cin >> n >> s;
  for (int i = 0; i < n; ++i) cin >> maps[i][0] >> maps[i][1];
  swap(maps[0][0], maps[s - 1][0]);
  swap(maps[0][1], maps[s - 1][1]);//把起点交换到开头位置
  for (int i = 0; i < n; ++i)
    for (int j = i + 1; j < n; ++j)
      path[tri(i, j)] =
          abs((ll)maps[i][0] - maps[j][0]) + abs((ll)maps[i][1] - maps[j][1]);//没卵用的预处理
  int end = (1 << n) - 1;
  memset(dp, 0x3f, sizeof(dp));
  dp[0][1] = 0;
  for (int status = 1; status <= end; ++status)//循环,找点,判断修改
    for (int i = 0; i < n; ++i)
      if (status >> i & 1)
        for (int j = i + 1; j < n; ++j)
          if (status >> j & 1)
            dp[j][status] =
                min(dp[j][status], dp[i][status ^ (1 << j)] + path[tri(i, j)]),
            dp[i][status] =
                min(dp[i][status], dp[j][status ^ (1 << i)] + path[tri(i, j)]);
  ll result = LLONG_MAX;
  for (int i = 0; i < n; ++i) result = min(result, dp[i][end]);//在终点路径中找最小
  cout << result;
}

https://acm.uestc.edu.cn/problem/wa-kuang-gong-lue

这道题有一个很神奇的特点,要使某个点最终走向西/北边,则其西/北边的点最终都会走向西/北边,因此,如果知道西北角一块矩形范围(从$(0,0)$到$(x,y)$,记作$dp[x][y]$内的最优解,则可以根据这一规律往东或者往南扩展得到$dp[x+1][y]$(假设取红矿)和$dp[x][y+1]$(假设取黑矿),反过来,$dp[x][y]$可以由$dp[x-1][y]$(取红矿)或$dp[x][y-1]$(取黑矿)得到。为取最大值,有$dp[x][y]=max(dp[x-1][y]+red[x][y],dp[x][y-1]+black[x][y])$,根据该式循环递推可得出结果

代码及注释:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
#include <bits/stdc++.h>
using namespace std;
int main() {
  int n, m;
  ios::sync_with_stdio(0);
  cin.tie(0);
  cin >> n >> m;
  long long red[n][m], black[n][m];
  for (int i = 0; i < n; ++i)
    for (int j = 0; j < m; ++j) {//预处理
      cin >> red[i][j];
      if (j) red[i][j] += red[i][j - 1];
    }
  for (int i = 0; i < n; ++i)
    for (int j = 0; j < m; ++j) {
      cin >> black[i][j];
      if (i) black[i][j] += black[i - 1][j];
    }
  long long dp[n][m];
  memset(dp, 0, sizeof(dp));
  **dp = max(**red, **black);
  for (int i = 1; i < n; ++i)//先算西北边上的
    dp[i][0] = max(dp[i - 1][0] + red[i][0], black[i][0]);
  for (int i = 1; i < m; ++i)
    dp[0][i] = max(red[0][i], dp[0][i - 1] + black[0][i]);
  for (int i = 1; i < n; ++i)//递推求解
    for (int j = 1; j < m; ++j)
      dp[i][j] = max(dp[i - 1][j] + red[i][j], dp[i][j - 1] + black[i][j]);
  cout << dp[n - 1][m - 1];
}

https://acm.uestc.edu.cn/problem/shou-ban

这题是我瞎jb写WA得最多的一题。首先这题可以分为两个子问题,即取出最多 $n$ 个元素的最佳方案问题;以及把它们插入合适的位置的最佳方案问题。第二个是简单的贪心问题,都插到相同高度的元素旁边,如果找不到,就使混乱度+1,插入到首尾或者两个不同元素间均可,最后增加的混乱度等于在第一个子问题中被全部取出的元素种数。而第一个子问题则是一个复杂的背包问题,状态可以定义为:当前处理到的位置 $i$,前i个中留下的个数 $v$,上一个的高度 $lst$,留下的字母中存在的高度集 $status$(二进制),共四维。总数据范围不算大,可以刷一遍所有状态求解,时间复杂度$O(n^2)$(常数有$8\times2^8$)。而问题的关键就在于状态的转移,要根据状态进行较为复杂的分类处理。详见以下注释:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
#include <bits/stdc++.h>
using namespace std;
using ll = long long;
int h[100];
ll dp[2][100][8][1 << 8];
bool arrivable[2][100][8][1 << 8];
int all = 0;
inline int c(int x) {//求被全部取出的元素种数,解决第二个子问题
  x = all - x;
  int ans = 0;
  while (x) {
    ans += x & 1;
    x >>= 1;
  }
  return ans;
}
int main() {
  int n, k;
  ios::sync_with_stdio(0);
  cin.tie(0);
  int Case = 0;
  while (cin >> n >> k && n + k) {
    memset(dp, 0x3f, sizeof(dp));
    /*for (int lst = 0; lst < 8; ++lst) {
      dp[1][0][lst][0] = 0;
      dp[0][0][lst][0] = 0;
    }*///后面写好后这段初始化已经不必要了
    all = 0;
    for (int i = 0; i < n; ++i) {
      int tmp;
      cin >> tmp;
      h[i] = tmp - 114514;
      all |= 1 << h[i];
    }//记录总元素集合(二进制)
    if (n - k <= c(0)) {//特判,不写也不会错,不过写了应该可以提速
      cout << "Case " << ++Case << ": " << c(0) << endl << endl;
      continue;
    }
    for (int i = 0; i < n; ++i) {
      if (i + 1 - k <= 1) {
        for (int lst = 0; lst < 8; ++lst)
          dp[i % 2][1][lst][1 << lst] = dp[!(i % 2)][1][lst][1 << lst];
        dp[i % 2][1][h[i]][1 << h[i]] = 1;//这里单独处理v=1的情况,不需要初始化v=0的情况
      }
      for (int v = max(i + 1 - k, 2); v <= i + 1; ++v)//由于最少要留下n-k个,循环下界为i + 1 - k,即在后面全选的情况下可以至少选到n-k个
        for (int status = 1; status < (1 << 8); ++status)
          if ((status | all) == all) {//不可达的状态直接跳过,执行下方else语句,赋值LLONG_MAX >> 2,防止出现后面加数后变负数的情况
            dp[i % 2][v][h[i]][status] = LLONG_MAX >> 2;
            for (int lst = 0; lst < 8; ++lst)
              if (status >> lst & 1) {//与上一个元素相等,则无论是否保留该元素,状态和混乱度都不变,在两者中选较小值
                if (h[i] == lst)
                  dp[i % 2][v][h[i]][status] =
                      min(dp[i % 2][v][h[i]][status],
                          min(dp[!(i % 2)][v - 1][lst][status],
                              dp[!(i % 2)][v][lst][status]));
                else {
                  dp[i % 2][v][lst][status] = dp[!(i % 2)][v][lst][status];//这些lst只能不保留h[i],混乱度和上一次循环相同
                  if ((status >> h[i]) & 1)//考虑从不同状态加入h[i]后进入该状态的情况,选择最小值并加上1
                    dp[i % 2][v][h[i]][status] = min(
                        dp[i % 2][v][h[i]][status],
                        min(dp[!(i % 2)][v - 1][lst][status],
                            dp[!(i % 2)][v - 1][lst][status ^ (1 << h[i])]) +
                            1);
                }
              } else
                dp[i % 2][v][lst][status] = LLONG_MAX >> 2;
          }
    }
    ll Min = LLONG_MAX;//从所有可达的,满足题目限制的点中找到最优解
    for (int v = n - k; v <= n; ++v)
      for (int lst = 0; lst < 8; ++lst)
        for (int status = 0; status < (1 << 8); ++status)
          if ((status | all) == all)
            Min = min(Min, dp[!(n % 2)][v][lst][status] + c(status));
    cout << "Case " << ++Case << ": " << Min << endl << endl;
  }
}

https://acm.uestc.edu.cn/problem/xu-lie

K题中的数列是两个方向上升子序列的叠加。对于上升子序列,我们可以维护一个单调递增的数组$dp[]$,用$dp[i]$表示已有的值最小的长度为 $i+1$的子序列末尾值,这样$dp$数组也是单调上升的,对于后面新增的每一个数$num[i]$,$dp[]$中比它小的数和它组成了以$num[i]$结尾的最长上升子序列,用$num[i]$替换第一个比它大的数,不断维护这个数组即可。我们用这个方法,从所有两个方向,对每个数求以其结尾的最长上升子序列$dpl[i]$和$dpr[i]$,以每个数为中心的最长oy序列长度$=min(dpl[i],dpr[i])*2-1$,最后遍历找最大值即可。

代码及注释:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
#include <bits/stdc++.h>
using namespace std;
int main() {
  int n;
  scanf("%d", &n);
  int num[n], dpl[n], dpr[n];
  for (int i = 0; i < n; ++i) {
    scanf("%d", num + i);
    dpl[i] = dpr[i] = INT_MAX;
  }
  int rpos = 0, lpos = 0;
  *dpl = *num, *dpr = num[n - 1];
  int llen[n], rlen[n];//加入两端值
  *llen = 1, rlen[n - 1] = 1;
  for (int i = 1; i < n; ++i) {
    int ir = n - 1 - i;//从右端遍历
    if (num[i] > dpl[lpos]) {//处理左端
      dpl[++lpos] = num[i];
      llen[i] = lpos + 1;
    } else {
      int p = lower_bound(dpl, dpl + lpos + 1, num[i]) - dpl;
      dpl[p] = num[i];
      llen[i] = p + 1;
    }
    if (num[ir] > dpr[rpos]) {//处理右端
      dpr[++rpos] = num[ir];
      rlen[ir] = rpos + 1;
    } else {
      int p = lower_bound(dpr, dpr + rpos + 1, num[ir]) - dpr;
      dpr[p] = num[ir];
      rlen[ir] = p + 1;
    }
  }
  int result = 1;
  for (int i = 0; i < n; ++i)
    result = max(result, min(llen[i], rlen[i]) * 2 - 1);//遍历节点找出最大结果
  printf("%d", result);
}

https://acm.uestc.edu.cn/problem/shen

该题可以先用一组集合$A[]$处理每组含有的字母有哪些,然后该题把状态定义为当前处理到第i段,以及最后一个字母j,通常情况下有$dp[i][j]=min(dp[i-1][x]+A[i].size()), x\in A[i-1]$.然后本题的重点是特判,通过观察和假设不难得出,如果$A[i]$中含有$x$并且满足:$j \neq x$或$A[i].size()=1$则该值减一(先减再取最值),然后在dp数组的最后一行取最小值,即可得出答案。

代码及注释:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
#include <bits/stdc++.h>
using namespace std;
int main() {
  ios::sync_with_stdio(0);
  cin.tie(0);
  int t;
  cin >> t;
  while (t--) {
    int k;
    string s;
    cin >> k >> s;
    set<int> a[s.length() / k];
    int dp[s.length() / k][26];
    memset(dp, 0x7f, sizeof(dp));
    for (int i = 0; i < s.length(); ++i) a[i / k].insert(s[i] - 'a');//将字母处理为 0~25 ,避免浪费空间
    for (int p : a[0]) dp[0][p] = a[0].size();//第1组不存在特判
    for (int i = 1; i < s.length() / k; ++i) {//循环,特判,转移
      for (auto p : a[i]) {
        for (auto q : a[i - 1]) {
          int ans = dp[i - 1][q] + a[i].size();
          if (a[i].find(q) != a[i].end() && (a[i].size() == 1 || p != q)) --ans;
          dp[i][p] = min(dp[i][p], ans);
        }
      }
    }
    int Min=INT_MAX;
    for (auto p : dp[s.length() / k - 1]) Min = min(Min, p);//找出最终答案
    cout << Min << endl;
  }
}

https://acm.uestc.edu.cn/problem/wei-ming-ou-yi-lang

这也是一道状压dp的题目。二进制数存储当前到达过的点,然后循环刷表,刷过去就完事儿了,也没有必要全部预处理,在循环内部遍历一下到过的点,得到可以到的点,然后枚举更新值即可,$O(2^n nT)$时间也完全足够。刷表完之后,$dp[(i«n)-1]$的值即为答案。另外,本题可用的小知识点:bitset用于二进制输入

代码及注释:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
#include <bits/stdc++.h>
using namespace std;
int main() {
  int t;
  ios::sync_with_stdio(0);
  cin.tie(0);
  cin >> t;
  for (int ca = 1; ca <= t; ++ca) {
    int n;
    cin >> n;
    bitset<16> init, add[n];
    cin >> init;
    for (int i = 0; i < n; ++i) cin >> add[n - 1 - i];//用bitset变量,可以直接cin输入二进制数,然后用其成员函数转换成其他类型
    unsigned long long dp[1 << n];
    memset(dp, 0, sizeof(dp));
    *dp = 1;//初始化,求方案数的题常常为1而不是0,因为只有一个点也能构成一个方案
    for (int i = 0; i < (1 << n); ++i)//开始刷表
      if (dp[i]) {
        unsigned long path = init.to_ulong();
        for (int j = 0; j < n; ++j)
          if (i >> j & 1) path |= add[j].to_ulong();
        for (int j = 0; j < n; ++j)
          if (!(i >> j & 1))
            if (path >> j & 1) dp[i ^ (1 << j)] += dp[i];
      }
    cout << "Case " << ca << ": " << dp[(1 << n) - 1] << endl;
  }
}

https://acm.uestc.edu.cn/problem/zi-chuan

这题的状态定义很浅显易懂,即在前 $i$ 个数中,除k的余数为 $j$ ,这样的字串个数记为 $dp[i][j]$。转移方程也很简单,$j$ 乘10,再加上下一个数后求余,把个数加到对应的状态,即 $dp[i][j]$ 的值应加到 $dp[i+1][(j*10+num[i+1])%k]$ 上,再考虑下个数单独做一个字串的情况即可。最终答案为 $\sum_{i=0}^{n-1}dp[i][0]$,因此循环中可以维护一下sum值。而这题 $dp[i]$ 只与 $dp[i-1]$ 有关,所以采用滚动数组优化,节省空间

代码及注释:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#include <bits/stdc++.h>
using namespace std;
int main() {
  int n, k;
  ios::sync_with_stdio(0);
  cin.tie(0);
  cin >> n >> k;
  char num[n + 1];
  cin >> num;
  for (auto &p : num) p -= '0';//方便后面直接做数组下标
  unsigned long long dp[2][k];
  int a[n];
  memset(dp, 0, sizeof(dp));
  unsigned long long sum = 0;
  bool now = 0;
  for (int i = 0; i < n; ++i) {
    for (int j = 0; j < k; ++j) dp[now][(j * 10 + num[i]) % k] += dp[!now][j];//把每一个量向后转移
    ++dp[now][num[i] % k];//num[i]单独成字串
    memset(dp[!now], 0, sizeof(dp[!now]));
    sum += dp[now][0];
    now = !now;
  }
  cout << sum;
}

https://acm.uestc.edu.cn/problem/vanyou-xi

这是一道树形D(深)P(搜)的题目,首先是有向图建树,这个只要根据输入把节点用一个邻接链表连起来就能用,再顺便维护一下入度(是否为零即可),找到根节点。然后就是写一个深搜,在递归调用返回值中选择最值来转移状态,由于两人策略不同(重载运算符后,一人要最大值,另一人要最小值),可以用一个bool变量作为参数记录当前轮到谁,每次调用时更换该参数即可。最终返回到main()函数的值即为结果。虽然深搜但实际上时间复杂度也就 $O(n)$

代码及注释:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
#include <bits/stdc++.h>
using namespace std;
using ll = long long;
list<int> a[1000000];
int num[1000000];
bool fa[1000000];
struct sc {//保存两人分数,重载运算符
  ll e, m;
  sc(){};
  sc(ll e, ll m) {
    this->e = e;
    this->m = m;
  }
  friend bool operator<(sc a, sc b) {
    return a.e == b.e ? a.m > b.m : a.e < b.e;
  }
};
sc dp(int pos, bool moe) {
  sc ans(0, 0);
  bool init = 1;
  if (moe) {
    for (auto &p : a[pos]) {//第一次赋值,之后比较再更新
      if (init) {
        ans = dp(p, !moe);
        init = 0;
        continue;
      }
      ans = max(ans, dp(p, !moe));
    }
    a[pos].clear();//强迫症清内存。。。
    return sc(ans.e, ans.m + num[pos]);
  }
  for (auto &p : a[pos]) {//思路与上面类似
    if (init) {
      ans = dp(p, !moe);
      init = 0;
      continue;
    }
    ans = min(ans, dp(p, !moe));
  }
  a[pos].clear();
  return sc(ans.e + num[pos], ans.m);
}
int main() {
  char start[4];
  int n;
  ios::sync_with_stdio(0);
  cin.tie(0);
  cin >> start >> n;
  bool moe = *start == 'm';
  for (int i = 0; i < n; ++i) cin >> num[i];
  for (int i = 0; i < n - 1; ++i) {//建立邻接链表
    int x, y;
    cin >> x >> y;
    a[x - 1].push_back(y - 1);
    fa[y - 1] = 1;//记下该点入度不为0
  }
  int root;
  for (int i = 0; i < n; ++i)
    if (!fa[i]) {//找根节点
      root = i;
      break;
    }
  sc result = dp(root, moe);
  cout << result.e << ' ' << result.m;
}

https://acm.uestc.edu.cn/problem/gong-lue-mei-zhi

这道题首先看起来很复杂,但实际就是一个背包问题。每个元素的属性可以简化成价值a,花费c,和达到先决条件所需花费d,其中在d上面的花费是公共的,这样就是两个子问题:分配先决条件d花费的最优解;以及在问题一的条件下,用剩余的钱获得最大价值的最优解。第一个子问题可以直接枚举,把元素按d排序,则第二个子问题是前几个元素的01背包问题,两个问题都很好解决。然而直接在循环枚举中写01背包时间复杂度高达$O(m n^2)$,结果当然是TLE。再分析问题,会发现存在很多被重复计算的值。所以只需跑一次01背包,在过程中导出问题一的各种情况下的最优解,即可在$O(mn)$内得到总的最优解。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
#include <bits/stdc++.h>
using namespace std;
using ll = long long;
using ull = unsigned long long;
struct M {
  int a;
  ll c, d;
  M() {}
  M(int a, ll c, ll d) {
    this->a = a;
    this->c = c;
    this->d = d;
  }
  bool operator<(M x) { return this->d < x.d; }//重载,按d排序
} target[5000];
int m;
int main() {
  int n, k, x, y;
  ios::sync_with_stdio(0);
  cin.tie(0);
  cin >> n >> m >> k >> x >> y;
  for (int i = 0; i < n; ++i) {
    int tmp;
    cin >> target[i].a >> tmp >> target[i].c >> target[i].d;
    target[i].c = max((target[i].c - tmp) * y, 0LL);
    target[i].d = max((target[i].d - k) * x, 0LL);
  }//输入并简化各值
  sort(target, target + n);
  ll dp[m + 1];
  memset(dp, 0, sizeof(dp));
  ll Max = 0;
  for (int i = 0; i < n; ++i) {//开始跑0-1背包
    if (m < target[i].d) break;
    for (int j = m; j >= target[i].c; --j)//倒序可以直接一维数组
      dp[j] = max(dp[j], dp[j - target[i].c] + target[i].a);
    Max = max(Max, dp[m - target[i].d]);//导出子问题最优解,与全局最优解比较更新
  }
  cout << Max;
}

https://acm.uestc.edu.cn/problem/chou-qia

首先,从数据范围可以看出,要求复杂度$O(logn)$进行递推,这很容易想到矩阵快速幂。这道题的难点就在这个矩阵上面。我之前想了很久,但是思维局限在二维或三维矩阵上,一直没有推出一个常矩阵。事实上这是一个m+1阶方阵,用一个列向量表示第i次碎片数量从0到m的概率。记作$dp[i]=(dp[i][0],dp[i][0]…dp[i][m])^\top$,然后就可以很容易的递推写出整个矩阵。其中可以把直接抽中定义为直接进入$dp[i+1][m]$状态,即在第最后一行各元素全部加上直接抽中的概率。这样得到的矩阵求快速幂,然后乘初始状态。由于初始状态(一次也没抽)的$dp[0]=(1,0,0,0…)^\top$,最后的结果即为矩阵n次幂最后一行的第一个数。做这题的时候写了一下矩阵的操作,暂时只写了需要的乘法操作,并且没有优化,准备以后有空再把它完善一下作为模板。

代码及注释:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
#include <bits/stdc++.h>
const long long M = 1000000007;
using namespace std;
struct martix_int {
  int w, h;
  long long **data;
  martix_int(int w, int h) {//这里的构造方式,既能data[i][j]访问,也能 memset(*data, 0, sizeof(long long) * h * w); 置零。
    this->w = w;
    this->h = h;
    data = new long long *[h];
    *data = new long long[h * w];
    for (int i = 1; i < h; ++i) data[i] = data[i - 1] + w;
    memset(*data, 0, sizeof(long long) * h * w);
  }
};
martix_int operator*(martix_int x, martix_int y) {//无优化的三层循环
  martix_int result = martix_int(x.h, y.w);
  for (int i = 0; i < x.h; ++i)
    for (int j = 0; j < x.w; ++j)
      for (int k = 0; k < y.w; ++k)
        result.data[i][k] =
            (result.data[i][k] + x.data[i][j] * y.data[j][k]) % M;
  return result;
}
martix_int Pow(martix_int x, int n) {//简单的递归快速幂
  martix_int ans(x.h, x.w);
  if (!n)
    for (int i = 0; i < x.w; ++i) ans.data[i][i] = 1;
  else if (n == 1)
    ans = x;
  else {
    martix_int tmp = Pow(x, n / 2);
    ans = tmp * tmp;
    if (n % 2) ans = ans * x;
  }
  return ans;
}
int main() {
  int n, m;
  ios::sync_with_stdio(0);
  cin.tie(0);
  cin >> n >> m;
  double tmp1, tmp2;
  cin >> tmp1 >> tmp2;
  long long p1 = round(tmp1 * 1000), p = round(tmp2 * 1000);//然而对于之前存在精度误差问题的数据,round()并没有卵用
  martix_int P(m + 1, m + 1);
  for (int i = 0; i <= m; ++i) P.data[i][i] = 1000 - (m - i) * p - p1;
  for (int i = 0; i <= m - 1; ++i) P.data[i + 1][i] = (m - i) * p;
  for (int i = 0; i <= m; ++i) P.data[m][i] += p1;
  P = Pow(P, n);
  cout << P.data[m][0];
}

https://acm.uestc.edu.cn/problem/zhde-jiang-bei

这道题是一个比较标准的斜率优化dp,首先看到静态的区间和,先把前缀和弄出来。 状态转移方程很简单,$dp[i]=min{dp[j]+(a-L)^2},j<i,a=sum[i]-sum[j]+i-j-1$ 但是直接遍历,$O(n^2)$的时间复杂度显然会TLE,所以需要一个快速找出$min{dp[j]+(a-L)^2}$的算法。我们把整个式子拆开,可以定义

$h(x)=sum[x]+x$

$g(x)=dp[x]+h^2(x)+2xL$

则点$(h(x),g(x))$可表示第x个元素在该题中判断优先级的一个依据。对于每一个i,找到第一个左斜率小于$k[i]=2h(i)$,右斜率大于$k[i]$的点,即为取得$min{dp[j]+(a-L)^2}$的点,即可算出$dp[i]$。最后的$dp[n-1]$即为所求。在这个过程中,左斜率大于右斜率的点显然不可能被取,可以直接去掉,又因为 $k[i]$ 和 $i$ 成正相关,所以 $i$ 从小到大循环时,右斜率小于 $k[i]$ 的点也可以直接删掉,后面不会再取到。这样就成了一个单调队列(和数据结构专题的问题D类似)。后来看了模板通常都喜欢用数组模拟双端队列,我这次写的deque(时间开销较大),不知道这类会不会有卡STL的题。

代码及注释:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
#include <bits/stdc++.h>
using namespace std;
using ll = long long;
int n, l;
ll sum[10000000], dp[10000000];
inline ll Pow(ll a) { return a * a; }
inline ll h(int i) { return sum[i] + i; }
inline ll g(int x) { return dp[x] + Pow(h(x)) + 2 * h(x) * l + 2 * h(x); }
inline bool compare1(int p, int q, int r) {
  return (g(p) - g(q)) * (h(q) - h(r)) < (h(p) - h(q)) * (g(q) - g(r));
}
inline bool compare2(int p, int q, int i) {
  return (g(q) - g(p)) < 2 * h(i) * (h(q) - h(p));
}//定义各个函数,简化后面操作。
int main() {
  ios::sync_with_stdio(0);
  cin.tie(0);
  cin >> n >> l;
  cin >> *sum;
  for (int i = 1; i < n; ++i) {//前缀和
    cin >> sum[i];
    sum[i] += sum[i - 1];
  }
  deque<int> rest;
  rest.push_front(0);
  dp[0] = Pow(h(0) - l);//第一组单独处理
  // for(int &p:rest)cout<<p<<endl;
  for (int i = 1; i < n; ++i) {
    while (rest.size() >= 2 &&
           compare2(rest[rest.size() - 1], rest[rest.size() - 2], i))//加判断防止越界
      // K(rest[rest.size() - 1], rest[rest.size() - 2]) < k(i))
      rest.pop_back();
    dp[i] = min(Pow(h(i) - l),
                dp[rest.back()] + Pow(h(i) - h(rest.back()) - 1 - l));//更新值
    while (
        rest.size() >= 2 &&
        compare1(i, rest[0], rest[1]))  // K(i, rest[0]) < K(rest[0], rest[1]))
      rest.pop_front();//加入新的点
    rest.push_front(i);
  }
  cout << dp[n - 1];
}