Loading

没有做过 Shopping Plans 的人生是不完整的。

没有做过 \(\text{Shopping Plans}\) 的人生是不完整的。

本质是关于一类求前 k 优解的问题。

Shopping Plans on Arrays

[CCO2020] Shopping Plans。

有 \(N\) 个商品,每个商品有个种类 \(a_i\),有个价格 \(c_i\)。

对于第 \(j\) 个种类,必须购买个数位于 \([x_j,y_j]\) 的商品,即最少购买 \(x_j\) 个,最多购买 \(y_j\) 个该种类的商品。

您需要求出前 \(K\) 种便宜的方案所需的价钱,如果没有这样的方案,请输出 -1。

特别的,如果有相同钱数,但是具体方案不相同的,算作两种方案。

思路

对于前 \(K\) 小方案问题。

我们可以考虑当前方案对下一个方案的转移。

重点在于转移的最优化与不重不漏。

只有一种种类

假设没有 \(l,r\) 的限制怎么做。

我们不妨把所有价格排序。

发现一种状态转移到另一种状态,无异与将其中已选择的一个物品不选,选择他后面的一个物品。

这样可以得到一个更劣的状态。

考虑使用 \((now, nxt, val)\) 来表示当前状态。

\(now\) 表示我们当前准备不选的物品。

\(nxt\) 表示 \(now\) 后面第一个选择的物品。

那么有转移:

\[(now,nxt,val)\rightarrow(now+1,nxt,val-c_{now}+c_{now+1}) \]

当然,这样的转移没有包括所有的物品。

那么我们不妨在加入一维。

考虑使用 \((pre, now, nxt, val)\) 来表示当前状态。

\(pre\) 表示 \(now\) 前面有几个元素,由于 \(pre\) 个元素都没有位移过,所以它必然是一段 \(1\sim pre\) 的前缀。

那么有转移:

\[(pre,now,nxt,val)\rightarrow(pre,now+1,nxt,val-c_{now}+c_{now+1}) \]

\[(pre,now,nxt,val)\rightarrow(pre-1,pre,now,val) \]

发现第二种转移与当前的方案是同一种,所以我们可以钦定每一次转移必须位移一次。

那么有:

\[(pre,now,nxt,val)\rightarrow(pre-1,pre+1,now,val-c_{pre}+c_{pre+1}) \]

初始状态为:

\[(i,i,n+1,sum_i) \]

至于 \(l,r\) 的限制也变得比较简单。

\[i\in[l,r],(i,i,n+1,sum_i) \]

有多种种类

可以发现,每一种种类之前是比较独立的。

所以我们同样可以用之前的方法求出单种种类的第 \(k\) 小。

设 \(kth_j(i)\) 为第 \(j\) 种颜色中,第 \(i\) 小的方案。

使用同样的状态维护外层的贪心。

\((now,k,val)\) 表示当前在第 \(now\) 个颜色,使用的是第 \(k\) 小方案。

那么有转移:

\[(now,k,val)\rightarrow(now,k+1,val-kth_{now}(k)+kth_{now}(k+1)) \]

\[(now,k,val)\rightarrow(now+1,2,val-kth_{now+1}(1)+kth_{now+1}(2)) \]

这样的转移还存在一点瑕疵。

我们有可能会跳过一些颜色。

那么对于 \(k=2,now\not = 1\),我们有转移:

\[(now,2,val)\rightarrow(now+1,2,val-(kth_{now+1}(1)-kth_{now+1}(2))+(kth_{now}(1)-kth_{now}(2))) \]

这样,我们把所有颜色按 \(kth(1)-kth(2)\) 从大到小排序,就可以正常转移了。

初始状态 \((1,1,\sum kth(1))\)。

时间复杂度:\(O(k\log k)\)。

Shopping Plans on Tree

给定一棵 \(n\) 个节点的带权无根树。

你需要求树上权值前 \(k\) 小的连通块。

思路

考虑点分治。

可以将问题转化为树上带根权值前 \(k\) 小连通块。

可以发现在树上的东西的选择具有一定的依赖性。

具体而言即儿子的选择依赖于父亲的选择。

那么不妨提出每个点的 \(\text{dfn}\) 序。

若是选,我们就连边:

\[(dfn_i,dfn_i+1,val_i) \]

若是不选,我们连边:

\[(dfn_i,low_i,0) \]

这样,问题转化为 \(1 \to n + 1\) 的 \(k\) 短路问题。

时间复杂度:\(O(n\log n^2 + k\log k)\)。

听上去简单很多,但代码要写很多板子(点分治,\(k\) 短路,都比较难写)。

Shopping Plans on Permutation

给定一个长度为 \(n\) 的排列 \(p\) 和数组 \(a\)。

定义一个排列 \(q\) 的代价为 \(\sum a_i \times q_i\),求代价前 \(k\) 小的排列。

思路

首先对 \(a_i\) 降序排列。

那么此时的最优解为 \(\sum a_i\times i\)。

然后再此基础上调整拓展。

很容易想到拓展方式就是交换相邻的升序对。

考虑让每个排列被唯一得到:于是从后往前考虑每个人,然后让它往后 \(\text{swap}\) 若干步。

可以记录 \((p,x)\) ,表示我们把现在轮到从前往后第 \(p\) 个人一直向前 \(\text{swap}\),搞到了第 \(x\) 个位置,这里有 \(x>p\)。

初始状态为 \((i,i+1)\)。

第一种转移,也就是往前 \(\text{swap}\) 一个位置。

\[(p,x) \to (p,x+1) \]

第二种转移,也就是换成前一个位置 \(\text{swap}\)。

\[(p,x) \to (p-1,p) \]

注意到这里计算贡献需要知道整个序列的实时位置,可以使用可持久化数组(线段树)来保存,所以实际是个三元组 \((p,x,rt)\)。

但是我们还需要假如一个反悔机制,也就是我们不一定在前一个位置必定转移。

记录四元组:

\[(pre,p,x,rt) \]

表示在前 \(pre\) 个位置不动时,我们把现在轮到从前往后第 \(p\) 个人一直向前 \(\text{swap}\),搞到了第 \(x\) 个位置,这里有 \(x>p\)。

并且设 \(f_x=a_{x}-a_{x+1}\),也就是交换一次的代价。

那么有转移:

\[(pre,p,x,rt_u) \to (pre,p,x+1,rt_v) \]

\[(pre,p,x,rt_u) \to (p-2,p-1,p,rt_v) \]

\[(pre,p,x,rt_u) \to (pre,u,u+1,rt_v) \]

其中 \(u\) 是小于 \(pre\) 的 \(f_u\) 最小值的位置。

可以发现在转移中 \(u < pre\),设置这一步的目的是什么呢。

这样可以方便的表示出在 \(1\sim pre\) 这一些数中,交换的只有 \(u\) 与 \(u+1\) 两个数。

那么就给反悔提供了条件。

有转移:

\[u\le pre,(pre,u,u+1,rt_u) \to (u-1,u,u+2,rt_v) \]

\[u\le pre,(pre,u,u+1,rt_u) \to (pre,v,v+1,rt_v) \]

其中 \(v\) 是第一个反悔后答案变大的点。

也就是在前缀 \(1\sim p-1\),\((f_u,u)\) 的后继。

这里可以用主席树上二分解决。

这样我们整个问题就基本解决了。

时间复杂度: \(O((n+k)\log n+k\log k)\)。

代码,这是碰都碰不了的话题
#include <bits/stdc++.h>
#define rep(i, a, b) for (int i = (a); i <= (b); i++)
#define per(i, a, b) for (int i = (a); i >= (b); i--)
#define op(x) ((x & 1) ? x + 1 : x - 1)
#define odd(x) (x & 1)
#define even(x) (!odd(x))
#define lc(x) (x << 1)
#define rc(x) (lc(x) | 1)
#define lowbit(x) (x & -x)
#define mp(x, y) make_pair(x, y)
typedef long long ll;
typedef unsigned long long ull;
typedef double db;
using namespace std;
const int MAXN = 1e5 + 10, MAXM = 5e7 + 10;

ll n, k, a[MAXN], rk[MAXN], arr[MAXN];

namespace DS {  // 维护前缀的主席树
const int MAXM = 2e6 + 10;
int rt[MAXN];
struct Node {
  int lc, rc;
} t[MAXM];
int tot;
int mdf(int x, int l, int r, int pos) {
  int p = ++tot;
  t[p] = t[x];
  if (l == r) return p;
  int mid = (l + r) >> 1;
  (pos <= mid) ? (t[p].lc = mdf(t[p].lc, l, mid, pos))
               : (t[p].rc = mdf(t[p].rc, mid + 1, r, pos));
  return p;
}
int qry(int x, int l, int r, int pos) {  // 二分处下一个为1的位置
  if (r <= pos || !x) return 0;
  if (l == r) return r;
  int mid = (l + r) >> 1;
  int ret = qry(t[x].lc, l, mid, pos);
  if (ret)
    return ret;
  else
    return qry(t[x].rc, mid + 1, r, pos);
}
void build() {
  rep(i, 1, n - 1) { rt[i] = mdf(rt[i - 1], 1, n - 1, rk[i]); }
}
};             // namespace DS
struct Node {  // 用来维护序列的主席树
  int lc, rc, val;
} t[MAXM];
int tot;
int build(int l, int r) {
  int x = ++tot;
  if (l == r) {
    t[x].val = l;
    return x;
  }
  int mid = (l + r) >> 1;
  t[x].lc = build(l, mid);
  t[x].rc = build(mid + 1, r);
  return x;
}
int mdf(int x, int l, int r, int pos, int val, int md) {
  int p = (!md) ? (++tot) : (x);
  t[p] = t[x];
  if (!p) p = ++tot;
  if (l == r) {
    t[p].val = val;
    return p;
  }
  int mid = (l + r) >> 1;
  (pos <= mid) ? (t[p].lc = mdf(t[p].lc, l, mid, pos, val, md))
               : (t[p].rc = mdf(t[p].rc, mid + 1, r, pos, val, md));
  return p;
}
int qry(int x, int l, int r, int pos) {
  if (l == r) return (t[x].val);
  int mid = (l + r) >> 1;
  return (pos <= mid) ? (qry(t[x].lc, l, mid, pos))
                      : (qry(t[x].rc, mid + 1, r, pos));
}
int mdf(int rt, int p, int x, int y) {
  int ret = mdf(rt, 1, n, p, x, 0);
  ret = mdf(ret, 1, n, p + 1, y, 0);
  return ret;
}
struct State {
  ll sum;
  bool operator<(const State& n2) const { return sum > n2.sum; }
  int now;  // 当前在移动的值为now
  int pos;  // 移动到了位置pos(pos>now)
  int cur;
  // 如果是正常转移,则cur = now-1,
  // 否则cur是可选取的前缀范围,意味着cur>=pos
  int rt;  // p序列
};
priority_queue<State> pq;
map<string, int> ok;
void ext(State ts) {
  ll sum = ts.sum;
  int now = ts.now, pos = ts.pos, cur = ts.cur, rt = ts.rt;
  // 1. 继续向后交换一步
  if (pos < n) {
    int nxt = qry(rt, 1, n, pos + 1);
    int nrt = mdf(rt, pos, nxt, now);
    ll nsum = sum + (nxt - now) * (a[pos] - a[pos + 1]);
    pq.push((State){nsum, now, pos + 1, now - 1, nrt});
  }
  // 2. 上一个人交换一步
  if (now > 1) {
    int hd = qry(rt, 1, n, now);
    int nrt = mdf(rt, now - 1, hd, now - 1);
    ll nsum = sum + (hd - now + 1) * (a[now - 1] - a[now]);
    pq.push((State){nsum, now - 1, now, now - 2, nrt});
  }
  // 3. 跳过上一个人,进入特殊状态
  if (now > 2) {
    int num = DS::qry(DS::rt[now - 2], 1, n - 1, 0);
    assert(num);
    num = arr[num];
    assert(num < now - 1);
    int nrt = mdf(rt, num, num + 1, num);
    ll nsum = sum + (a[num] - a[num + 1]);
    pq.push((State){nsum, num, num + 1, now - 1, nrt});
  }
  if (now <= cur) {  // 带反悔选项
    assert(pos == now + 1 && cur >= pos);
    // 3. 反悔
    int num = DS::qry(DS::rt[cur - 1], 1, n - 1, rk[now]);
    if (num) {
      num = arr[num];
      assert(num < cur);
      int nrt = mdf(rt, now, now, now + 1);
      nrt = mdf(nrt, num, num + 1, num);
      ll nsum = sum - (a[now] - a[now + 1]) + (a[num] - a[num + 1]);
      assert(nsum >= sum);
      pq.push((State){nsum, num, num + 1, cur, nrt});
    }
  }
}
int main() {
  ios::sync_with_stdio(false);
  cin >> n >> k;
  k--;
  rep(i, 1, n) cin >> a[i];
  sort(a + 1, a + 1 + n, greater<ll>());
  ll sum = 0;
  rep(i, 1, n) sum += a[i] * i;
  cout << sum << endl;
  if (!k) exit(0);
  rep(i, 1, n - 1) arr[i] = i;
  sort(arr + 1, arr + n,
       [&](int x, int y) { return a[x] - a[x + 1] < a[y] - a[y + 1]; });
  rep(i, 1, n - 1) rk[arr[i]] = i;
  int rt = build(1, n);
  DS::build();
  rep(i, 1, n - 1) {  // 第一步交换i和i+1,再往后的不变
    ll nows = sum + a[i] - a[i + 1];
    int nowrt = mdf(rt, i, i + 1, i);
    pq.push((State){nows, i, i + 1, i - 1, nowrt});
  }
  while (k--) {
    assert(pq.size());
    State now = pq.top();
    pq.pop();
    cout << now.sum << "\n";
    ext(now);
  }
  return 0;
}


posted @ 2024-02-23 21:45  JiaY19  阅读(3)  评论(0)    收藏  举报