快来踩爆这个蒟蒻吧|

Little_corn

园龄:1年1个月粉丝:11关注:17

2024-10-06 09:51阅读: 6评论: 0推荐: 0

DP 好题记录

Armor and Weapons

fi,j 是盔甲 i,武器 j 时的最少时间,转移是平凡的。但是 O(n2) 显然超时。

注意答案不会太大,因为当没有加成时,在 i 轮的情况下最多可以得到 Fibi+2 的盔甲或武器。由此可知答案大概在 O(logn) 的量级。

于是我们考虑换量:设 fi,j 是在通过 i 轮,盔甲为 j 时,我们最大能拿到什么武器。

初始化 f1,1=1。转移容易。

qwq
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N = 2e5 + 10;
int n, m, t;
map<pair<int, int>, int> EX;
int f[N], g[N];
int val(int i){
int res = i + g[i];
if(EX.count({i, g[i]})) res++;
return res;
}
signed main(){
ios::sync_with_stdio(0);
cin.tie(0); cout.tie(0);
bool swp = false;
cin >> n >> m >> t; if(n > m) swap(n, m), swp = true;
for(int i = 1; i <= t; i++){
int x, y; cin >> x >> y; if(swp) swap(x, y);
EX[make_pair(x, y)] = 1;
}
int ans = 0; g[1] = 1;
if(n == 1 && m == 1){cout << 0; return 0;}
while(++ans){
for(int i = 1; i <= n; i++) if(g[i]) f[i] = max(f[i], min(m, val(i))), f[min(n, val(i))] = max(f[min(n, val(i))], g[i]);
for(int i = n; i; i--) g[i] = max(g[i + 1], f[i]), f[i] = 0;
// cout << g[n] << "\n";
if(g[n] >= m){cout << ans; return 0;}
}
return 0;
}

Formalism for Formalism

首先考虑转化问题,我们称一个等价集合是一个集合元素全部等价的集合,问题转化成求集合数量。

但这样还不好做,于是我们考虑用一个集合中字典序最小的那个字符串来代替该集合,这是常见的 Trick

考虑从前往后填数,现在考虑第 i 位填什么数。由于我们填出来的数有字典序最小的限制,在某些状态下,有一些数我们可能是不能填的,但是我们并不知道我们可以填些什么数。

注意到可能填的数只有 10 种,我们可以直接将可以填的数加入到状态表示中。自然的,我们令 fi,S 为当填到第 i 位,下一位填数可行状态集表示为 S 时,最大数组长度为多少。

找到 fi,S 的前驱是困难的,于是我们考虑 “我为人人” 的刷表法。对于一个 fi1,S 这样的已知状态,我们用它来为 fi,S0 这样的状态做贡献。我们枚举第 i 位填的数,现在来计算 S0。容易发现,第 i+1 位不可填数 j,当且仅当 ji 可以交换且 j<i

于是我们预处理出每种状态的后继即可。

qwq
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N = 1e5 + 10, V = 10, mod = 998244353;
int n, m, nxt[(1 << V + 2)][V + 2], g[(1 << V + 2)], f[(1 << V + 2)];
bool con[V + 2][V + 2];
signed main(){
ios::sync_with_stdio(0);
cin.tie(0); cout.tie(0);
cin >> n >> m;
for(int i = 1; i <= m; i++){
int x, y; cin >> x >> y;
con[x][y] = con[y][x] = true;
}
g[0] = 1;
for(int S = 0; S < (1 << V); S++){
for(int i = 0; i < V; i++){
if(S & (1 << i)){nxt[S][i] = -1; continue;}
for(int j = 0; j < V; j++) if(con[i][j] && (j < i || (S & (1 << j)))) nxt[S][i] |= (1 << j);
}
}
for(int i = 1; i <= n; i++){
for(int S = 0; S < (1 << V); S++){
if(!g[S]) continue; //这个优化很重要!
for(int ths = 0; ths < V; ths++){
if(nxt[S][ths] != -1) f[nxt[S][ths]] = (f[nxt[S][ths]] + g[S]) % mod;
}
}
for(int i = 0; i < (1 << V); i++) g[i] = f[i], f[i] = 0;
}
int ans = 0;
for(int i = 0; i < (1 << V); i++) ans = (ans + g[i]) % mod;
cout << ans;
// system("pause");
return 0;
}

P3643 [APIO2016] 划艇

首先容易设计出朴素 DP:设 fi,j 为考虑前 i 个数,且 ai=j 时的方案数。

但是 j 可能很大,于是考虑优化状态个数。

如何优化呢?注意到题目中数值上限有 109,但是出现的数值只有 2n 个,进而我们考虑离散化。注意,我们为了方便处理,将每个 ri 都加一,再进行离散化。

令第 i 小的数对应原来的数是 rki,不妨设有 m 个不同的数。显然的,我们将 rk1 rkm 分为了 m1 个互不相交的区间。注意到一个原来的区间一定可以通过几个相邻的区间组合而成,从而可以优化状态个数:

fi,j 为考虑前 i 个数,且 ai[rkj,rkj+1) 的方案数。于是我们枚举它的前驱 lst,并且让 [lst+1,i] 区间中的任意 ak[rkj,rkj+1) 或等于 0(我们称不选为令 ak=0)。

但是区间中可能会有一些 [lk,rk][rkj,rkj+1),我们只能强制 ak=0,这些不需要考虑。我们只考虑剩下的区间,不妨设剩下的区间有 cnt 个。(接下来区间长度 len=rkj+1rkj

单独考虑是困难的,于是我们考虑对它们整体考虑。首先分析没有 0 的情况,可以发现如果我们选了 len 个不同数中的 ilst 个,序列排列方式将是唯一的,所以答案是 Clen,ilst+1。接下来在序列中加入 ilst 个零(因为 ai 不能为 0)。对他们进行标号 1....ilst,对于一个标号为 i0,我们将它放置在第 i 个位置。于是其实对于一个不同的选择集合,不难发现都有唯一的方式,于是有 Clen+cnt1cnt 放置方式。

从而我们可以得出状态转移方程:

fi,j=lst=0i1j=0j1flst,j×Clen+cnt1cnt

初始化 f0,0=1,答案 i=1nfi,m1

但这样是 O(n4) 的,容易发现可以用前缀和优化。具体的,我们令 si,j=j=0jfi,j。状态转移方程就为 fi,j=lst=1islst,j1×Clen+cnt1cnt。但是这样又有一个问题,C 的计算不是容易的。注意到 C(x,y)=C(x1,y1)xinv(y),而且当 cnt=1 时,这是容易计算的,于是我们可以边计算 f 边更新 C 的值。

qwq
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N = 500 + 10, mod = 1e9 + 7;
int n, m, f[N][2 * N], l[N], r[N], rk[2 * N], s[N][2 * N], C[N];
int qpow(int x, int y){
int ret = 1;
while(y){
if(y & 1) ret = ret * x % mod;
x = x * x % mod;
y >>= 1;
}
return ret;
}
signed main(){
ios::sync_with_stdio(0);
cin.tie(0); cout.tie(0);
cin >> n;
for(int i = 1; i <= n; i++) cin >> l[i] >> r[i], r[i]++, rk[i] = l[i], rk[i + n] = r[i];
sort(rk + 1, rk + 2 * n + 1); m = unique(rk + 1, rk + 2 * n + 1) - (rk + 1);
for(int i = 1; i <= n; i++) l[i] = lower_bound(rk + 1, rk + m + 1, l[i]) - rk, r[i] = lower_bound(rk + 1, rk + m + 1, r[i]) - rk;
for(int i = 0; i < m; i++) s[0][i] = 1;
for(int j = 1; j < m; j++){
int len = rk[j + 1] - rk[j], tot = 0; C[0] = len;
for(int i = 1; i <= n; i++) C[i] = (C[i - 1] * (i + len) % mod) * qpow(i + 1, mod - 2) % mod;
for(int i = 1; i <= n; i++){
tot = 0;
if(l[i] <= j && r[i] > j){
for(int lst = i - 1; lst >= 0; lst--){
f[i][j] = (f[i][j] + s[lst][j - 1] * C[tot] % mod) % mod;
if(l[lst] <= j && r[lst] > j) tot++;
}
}
s[i][j] = (s[i][j - 1] + f[i][j]) % mod;
}
}
int ans = 0;
for(int i = 1; i <= n; i++) ans = (ans + s[i][m - 1]) % mod;
cout << ans;
// system("pause");
return 0;
}
/*
f[i][j] : 前 i 个数,a[i] 在 [j, j + 1)
枚举使 [lst + 1, j] 都在 [rk[j], rk[j + 1]) 的 lst(0 <= lst < i)
f[i][j] = \sum{s[lst][j - 1] * C(rk[j] - rk[j - 1] + cnt - 1, cnt)}
初始化:f[0][0] = 1
C(rk[j + 1] - rk[j], 1) = rk[j + 1] - rk[j]
C(x + 1, y + 1) = jc[x + 1] * jcinv[y + 1] * jcinv[x - y]
C(x, y) = jc[x] * jcinv[y] * jcinv[x - y]
C(x + 1, y + 1) = C(x, y) * (x + 1) * inv[y + 1]
*/2024-07-06 21:45:00 星期六

Interesting Problem Easy/Hard

首先给出一个关键但很显然的性质:一个决策只被前面的决策影响。于是我们可以设计一个区间 DP:设 fl,r,k 为若 [1,l1] 进行了 k 次操作, 区间 [l,r] 最多可以进行多少操作。转移有两种:

  • 消掉 [l+1,r1] 后再消 (l,r)

  • 找到一个断点 k,先消 [l,k] 再消 [k+1,r](注意这里的先后顺序是不固定的,即假如 [l,k] 的可消除对数大于 [k+1,r] 所需要的消除对数 x,可以通过先操作 [l,k] x 次后在再进行 [k+1,r] 的消除操作)。

时间复杂度 O(n4),可以通过 Easy

优化是比较困难的,考虑进一步发掘题目中的性质。观察到答案一定是由一段段全部消除的区间拼接而成的。受到上面的启发,我们可以再次设计出一个 DPfl,r 是将区间 [l,r] 全部消除最少所需要 [1,l] 消除多少对。转移是容易的。如何计算答案呢?再设计一个 DP 即可。

qwq
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N = 800 + 10;
int n, f[N][N], g[N], a[N];
int Get(int id, int val){
int opt = (id - val) / 2;
if(opt > id / 2 || ((id - val) & 1) || val > id) return -1;
return opt;
}
void solve(){
cin >> n; memset(f, 0x3f, sizeof f); memset(g, 0, sizeof g); int INF = f[0][0];
for(int i = 1; i <= n; i++) cin >> a[i], f[i][i - 1] = 0;
for(int len = 2; len <= n; len += 2){
for(int l = 1; l + len - 1 <= n; l++){
int r = l + len - 1;
if(Get(l, a[l]) != -1 && Get(l, a[l]) >= f[l + 1][r - 1]) f[l][r] = Get(l, a[l]);
for(int k = l; k < r; k++){
f[l][r] = min(f[l][r], max(f[l][k], f[k + 1][r] - (k - l + 1) / 2));
}
// if(f[l][r] > (l - 1) / 2 || (r - l + 1) % 2) f[l][r] = INF;
// cout << l << " " << r << " " << f[l][r] << "\n";
}
}
for(int i = 2; i <= n; i++){
g[i] = g[i - 1];
for(int j = 1; j <= i; j++) if(g[j - 1] >= f[j][i]) g[i] = max(g[i], g[j - 1] + (i - j + 1) / 2);
// cout << g[i] << "\n";
}
cout << g[n] << "\n";
}
signed main(){
ios::sync_with_stdio(0);
cin.tie(0); cout.tie(0);
int T; cin >> T; while(T--) solve();
return 0;
}

ABC176F Brave CHAIN

容易有简单 O(n3) DP:设 fi,a,b 是前 3×i+2 个保留 (a,b) 时的最大答案。有一下三类转移:

  • 不换卡:fi,a,b=fi1,a,b+[c=d=e]

  • 换一张:fi,a,c=fi1,a,b+[b=d=e]

  • 换两张:fi,c,d=fi1,a,b+[a=b=e]

当然有一些类似的不予考虑。我们发现其实没有多少状态被影响到了,若被影响到的多,原因也是 c=d=e,于是猜测影响到的状态不多。接下来考虑什么样的状态 fi,a,b 可能会被这三类转移分别更新。

对于第一种,显然对于任意 fi,a,b 有没有被更新取决于 [c=d=e],而这三者又是固定的,于是维护一个全局加标记即可。

对于第二种,被影响到的状态只有 c 这一列,若 [b=d=e] 成立,此时转移过来的状态是固定的。若不满足,其实就是对于固定的 a,让 fi,a,c 更新为这一行的最大值,维护一下即可。

对于第三种,若满足 [a=b=e],两边都是确定的。直接转移即可。若不满足,显然等价于更新为全局最大值,也是维护一下即可。

qwq
#include<bits/stdc++.h>
using namespace std;
const int N = 2000 + 10;
int n, f[N][N], arr[N * 3];
void MAX(int& x, int y){if(x < y) x = y;}
struct opt{
int k0, k1, val;
};
void upd(int a, int b, int w){
MAX(f[a][b], w); MAX(f[a][0], w); MAX(f[0][0], w);
}
signed main(){
ios::sync_with_stdio(0);
cin.tie(0); cout.tie(0);
cin >> n; memset(f, -0x3f, sizeof f); int allsum = 0;
for(int i = 1; i <= 3 * n; i++) cin >> arr[i];
upd(arr[1], arr[2], 0); upd(arr[2], arr[1], 0);
for(int i = 1; i < n; i++){
queue<opt> Q; int c = arr[3 * i], d = arr[3 * i + 1], e = arr[3 * i + 2], ad = 0;
// f[i][a][b]
if(c == d && d == e) ad++, allsum++;
// f[i][a][c], f[i][a][d], f[i][a][e]
for(int a = 1; a <= n; a++){
if(d == e) Q.push((opt){a, c, f[a][d] + 1});
Q.push((opt){a, c, f[a][0]});
if(c == e) Q.push((opt){a, d, f[a][c] + 1});
Q.push((opt){a, d, f[a][0]});
if(d == c) Q.push((opt){a, e, f[a][d] + 1});
Q.push((opt){a, e, f[a][0]});
}
// f[i][c][d], f[i][c][e], f[i][d][e]
Q.push((opt){c, d, f[e][e] + 1}); Q.push((opt){c, d, f[0][0]});
Q.push((opt){c, e, f[d][d] + 1}); Q.push((opt){c, e, f[0][0]});
Q.push((opt){d, e, f[c][c] + 1}); Q.push((opt){d, e, f[0][0]});
// update
while(!Q.empty()){
opt ths = Q.front(); Q.pop();
int a = ths.k0, b = ths.k1, w = ths.val - ad;
upd(a, b, w); upd(b, a, w);
}
}
int ans = 0;
for(int a = 1; a <= n; a++){
for(int b = 1; b <= n; b++){
MAX(ans, f[a][b] + (a == b && b == arr[n * 3]));
}
}
cout << ans + allsum;
return 0;
}
/*
f[i][a][b] = f[i - 1][a][b] + [c = d = e]
f[i][a][c] = f[i - 1][a][b] + [b = d = e]
f[i][c][d] = f[i - 1][a][b] + [a = b = e]
*/

AGC007E Shik and Travel

非常厉害的一道题!

首先容易发现答案具有单调性,于是先通过二分答案 V 转化成为判定问题。

接下来注意到一条边只能走两次,其实就是限制了当进入一棵子树,必须先把里面的所有点经过之后才能离开这棵子树。也就是说,当处理到一颗子树时,需要把它的一颗子树处理完,然后从一个叶子到另一颗子树的叶子,再处理完这个子树,然后离开。

不难发现我们只需要知道两颗子树从 u 到第一个叶子的距离以及最后一个叶子到 u 的距离。于是容易设计出状态 fu,a,b 表示是否有一种处理方案满足费用 V,且 u 到第一个叶子的距离为 a,最后一个叶子到 u 的距离为 b。状态转移就是

fu,a,b=fls,ada,ida&frs,jdb,bdb&[i+jV]

但是想要再优化就没有那么简单了。首先需要注意到一个显然的但是容易被忽略的性质:若 x1x2,y1y2fx2,y2fx1,y1 严格偏序,即没有用。于是我们可以将这些状态删掉,只记录有用的状态中值为 1 的,以减小时间复杂度。此时对于一个 u,其有用的 f 值显然是满足 a 单调递增,b 单调递减。于是可以双指针合并一下两颗子树中的信息。

但是这样的时间复杂度?我们设节点 u 中储存的有用的状态数为 sizu。一次转移产生的状态数上限是 2×min(sizls,sizrs),不难发现这其实等价于一个启发式合并,于是时间复杂度为 O(nlogn)

code:

qwq
#include<bits/stdc++.h>
#define ll long long
#define pir pair<ll, ll>
using namespace std;
const int N = 2e5 + 10;
int n, son[N][3];
ll dis[N][2];
vector<pir> f[N], g[N], vec[N];
ll lim;
void clrvec(vector<pir> &v){vector<pir> __qwq; swap(__qwq, v);}
void Merge_array(int u){
int j = 0; vector<pir> tmp;
for(int i = 0; i < f[u].size(); i++){
while(j < g[u].size() && (g[u][j].first < f[u][i].first || (g[u][j].first == f[u][i].first && g[u][j].second < f[u][i].second))) tmp.push_back(g[u][j++]);
tmp.push_back(f[u][i]);
}
while(j < g[u].size()) tmp.push_back(g[u][j++]);
if(tmp.empty()) return;
int ttt = 0; vec[u].push_back(tmp[0]);
for(int i = 1; i < tmp.size(); i++){
if(tmp[i].second < vec[u][ttt].second) ttt++, vec[u].push_back(tmp[i]);
}
}
void dfs(int u){
if(!son[u][2]){ vec[u].push_back(make_pair(0ll, 0ll)); return;}
for(int i = 0; i < 2; i++) dfs(son[u][i]);
int ls = son[u][0], rs = son[u][1], j = 0, sum = dis[u][0] + dis[u][1];
for(int i = 0; i < vec[ls].size(); i++){
while(j < vec[rs].size() && vec[ls][i].second + vec[rs][j].first + sum <= lim) j++;
if(j) f[u].push_back(make_pair(vec[ls][i].first + dis[u][0], vec[rs][j - 1].second + dis[u][1]));
}
swap(ls, rs); j = 0;
for(int i = 0; i < vec[ls].size(); i++){
while(j < vec[rs].size() && vec[ls][i].second + vec[rs][j].first + sum <= lim) j++;
if(j) g[u].push_back(make_pair(vec[ls][i].first + dis[u][1], vec[rs][j - 1].second + dis[u][0]));
}
Merge_array(u); clrvec(f[u]); clrvec(g[u]);
//cout << u << " " << vec[u].size() << " qwq" << "\n";
//for(int i = 0; i < vec[u].size(); i++) cout << vec[u][i].first << " " << vec[u][i].second << "\n";
}
bool check(ll V){
//cout << "\n" << V << "\n" << ":::" << "\n";
for(int i = 1; i <= n; i++) clrvec(f[i]), clrvec(g[i]), clrvec(vec[i]);
lim = V; dfs(1);
return vec[1].size();
}
signed main(){
ios::sync_with_stdio(0);
cin.tie(0); cout.tie(0);
cin >> n;
for(int i = 2; i <= n; i++){
int x, y; cin >> x >> y;
son[x][son[x][2]] = i; dis[x][son[x][2]] = y; ++son[x][2];
}
ll l = 0, r = 3e10, ans = 3e10;
while(l <= r){
ll mid = (l + r >> 1);
if(check(mid)) r = mid - 1, ans = mid;
else l = mid + 1;
}
cout << ans;
return 0;
}

P6383 『MdOI R2』Resurrection

首先观察答案形态,不难发现等价于每次找到当前树上两点所在连通块的根进行连边。现在考虑断边 (u,v) 形成的 G 的形态,可以发现这个东西只跟 1v 上每条边断边时间的相对关系有关。现在考虑这些边断边的相对时间 t0,t1,...,tm 以及点 u1,u2,....,um,观察出 um=u 会与 up(p=max{p|tp<tm}) 连边。那么当 t0,...,tm1 确定时,不同的 tm 可以造成不同的图 G 的个数为 i=0m1[ti>maxj=0i1{tj}]。其实就是 t0,...,tm 单调递增的单调栈大小,那么当 u 的单调栈大小为 wu 的时候,v 的单调栈大小就可以是 [2,wu+1]

所以不难设计出 DP 状态:fu,i 表示 1u 的单调栈大小为 i 时,u 子树内不同方案数。根据上面的分析,有转移 fu,i=vson(u)j=2i+1fv,j。前缀和优化即可做到 O(n2)

qwq
#include<bits/stdc++.h>
#define ll long long
#define pb emplace_back
using namespace std;
const int N = 3e3 + 10, mod = 998244353;
int n;
ll f[N][N], s[N];
vector<int> G[N];
int Mod(int x){return (x % mod + mod) % mod;}
void dfs(int u, int fa){
for(int j = 1; j <= n; j++) f[u][j] = 1;
if(u != n) f[u][1] = 0;
for(auto v : G[u]){
if(v == fa) continue; dfs(v, u);
for(int j = 1; j <= n; j++) s[j] = (s[j - 1] + f[v][j]) % mod;
for(int j = 1; j <= n; j++) f[u][j] = f[u][j] * Mod(s[j + 1] - s[1]) % mod;
}
// for(int i = 1; i <= n; i++) cout << u << " " << i << " " << f[u][i] << "\n";
}
signed main(){
ios::sync_with_stdio(0);
cin.tie(0); cout.tie(0);
cin >> n;
for(int i = 1; i < n; i++){
int x, y; cin >> x >> y;
G[x].pb(y); G[y].pb(x);
}
dfs(n, 0); cout << f[n][1] << "\n";
return 0;
}

P6846 [CEOI2019] Amusement Park

首先不难注意到假如一个有向图用 c 次操作定向成了一个 DAG,那么一定可以用 mc 次操作再把它定向成反的 DAG,所以问题转化为求定向后生成 DAG 数量,最后再乘上 m2

考虑 DP,并进行 子结构划分即考虑怎么把一个 DAG 扣掉一些点之后还变成一个 DAG。不难发现可以枚举 0 度点,但是这样还可能会算重。再考虑什么时候会算重,不难发现一个 0 度点集合 S,所有的子集都会被多算一遍,那么直接子集容斥即可。

时间复杂度 O(3n)

qwq
#include<bits/stdc++.h>
#define ll long long
using namespace std;
const int N = 20 + 1, M = N * N + 10, maxS = (1ll << N) + 10;
const ll mod = 998244353;
void ADD(ll &x, ll y){
x += y; (x >= mod) ? (x -= mod) : x;
}
int n, m, con[N], pd[maxS], pc[maxS];
ll f[maxS];
signed main(){
ios::sync_with_stdio(0);
cin.tie(0); cout.tie(0);
cin >> n >> m;
for(int i = 1; i <= m; i++){
int x, y; cin >> x >> y; x--; y--;
con[x] |= (1ll << y); con[y] |= (1ll << x);
} f[0] = 1;
for(int S = 1; S < (1ll << n); S++){
int st = 0; pc[S] = pc[S >> 1] + (S & 1);
for(int i = 0; i < n; i++) if(S & (1ll << i)) st |= con[i];
pd[S] = (!(S & st)); //cout << S << " " << st << " " << pd[S] << "\n";
for(int S0 = S; S0; S0 = S & (S0 - 1)){
if(!pd[S0]) continue;
//cout << S << " " << S0 << " " << f[S ^ S0] << "\n";
ADD(f[S], f[S ^ S0] * ((pc[S0] & 1ll) ? 1ll : (mod - 1ll)) % mod);
}
//cout << S << " " << f[S] << "\n";
} cout << f[(1ll << n) - 1] * m % mod * ((mod + 1) / 2) % mod;
return 0;
}

P10197 [USACO24FEB] Minimum Sum of Maximums P

首先记一个转化:max(a,b)=12(a+b+|ab|)

在两边插入两个固定的极大值,然后前面的 a+b 都是确定的了,最后把极大值的贡献剔除即可。问题转化为求 min|aiai+1|

考虑每段内如何排列最好,显然是升序排序最好(降序的话就把 L,R 交换就一样了),那么每段的贡献就是 |Lma|+|Rmi|+mami。不难发现 ma 的贡献随着其本身的增大而增大,mi 则反之。因此我们想让 ma 变小,mi 变大。记每一段的"区间"为 [mii,mai],于是可以得到结论,任意两个不同区间,要么相离要么包含。反证法即可。

因此我们可以设计一个区间 DPfl,r,s 表示已经填了 [l,r] 区间的数(排序后的),s 集合中的段已经被填满了。转移如下:

  • 合并相离的段:fl,r,s=fl,l+siz[s0]1,s0+fl+siz[s0],r,ss0,s0s

  • 确定一个段 i 的最大最小值:

    • 确定的段长度只有 1(这个情况只有在只剩一个数的时候使用,即 l=r,最后合并即可,否则转移正确性无法保证):fl,r,s=|Rial|+|alLi|

    • 一般情况:fl,r,s=fl+1,r1,s+|Rial|+aral+|Liar|

时间复杂度 O(3nn2)

qwq
#include<bits/stdc++.h>
#define ll long long
using namespace std;
const int N = 300 + 5, MAXV = (1ll << 7) + 5, INF = 1e6 + 5;
int n, m, a[N], id[N], L[N], R[N], len[N], siz[MAXV], cnt, vis[N];
ll ans = 2 * INF, f[N][N][MAXV];
void chkmin(ll& x, ll y){if(y < x) x = y;}
signed main(){
ios::sync_with_stdio(0);
cin.tie(0); cout.tie(0);
memset(f, 0x3f, sizeof f);
cin >> n >> m; n += 2; a[1] = a[n] = INF; m += 2; id[1] = 1; id[m] = n;
for(int i = 2; i < n; i++) cin >> a[i], ans += 2 * a[i];
for(int i = 2; i < m; i++) cin >> id[i], id[i]++, vis[id[i]] = 1;
for(int i = 1; i < m; i++){
if(id[i + 1] == id[i] + 1) ans += abs(a[id[i]] - a[id[i] + 1]);
else{
len[cnt] = id[i + 1] - id[i] - 1;
L[cnt] = a[id[i]], R[cnt] = a[id[i + 1]];
if(L[cnt] > R[cnt]) swap(L[cnt], R[cnt]);
cnt++;
}
}
int tt = 0;
for(int i = 2; i < n; i++) if(!vis[i]) a[++tt] = a[i];
sort(a + 1, a + tt + 1); n = tt;
for(int i = 1; i <= n; i++) f[i + 1][i][0] = 0;
for(int s = 0; s < (1ll << cnt); s++)
for(int i = 0; i < cnt; i++){
if(s & (1ll << i)) siz[s] += len[i];
}
for(int lll = 1; lll <= n; lll++){
for(int l = 1; l + lll - 1 <= n; l++){
int r = l + lll - 1;
for(int s = 0; s < (1ll << cnt); s++){
//if(s == 0) f[l][r][s] = 0;
f[l][r][s] = min(f[l + 1][r][s], f[l][r - 1][s]);
if(siz[s] > r - l + 1) continue;
for(int s0 = s; s0; s0 = s & (s0 - 1)) chkmin(f[l][r][s], f[l][l + siz[s0] - 1][s0] + f[l + siz[s0]][r][s ^ s0]);
for(int i = 0; i < cnt; i++){
if((1ll << i) & s){
ll val = abs(R[i] - a[r]) + a[r] - a[l] + abs(a[l] - L[i]);
if(siz[s] == 1 && len[i] == 1) chkmin(f[l][r][s], val);
else if(len[i] > 1) chkmin(f[l][r][s], f[l + 1][r - 1][s ^ (1ll << i)] + val);
}
}
//cout << l << " " << r << " " << s << " " << f[l][r][s] << "\n";
}
}
}
cout << (ans + f[1][n][(1ll << cnt) - 1]) / 2 - 2 * INF;
return 0;
}

AGC061E Increment or XOR

很好的一道划分 DP 阶段的题。

首先观察结构,发现 +1 等价于把后面的一段 1 变为 0,然后假如继续 +1,那么相当于一个一样的阶段(从 02k10 ),即 子问题。且当 +1 没有达到 i 位时,i 以上的位都不会被影响到。

于是我们考虑把前 i 位的操作划分为一个个阶段,不难发现这一个一个阶段的起点都可以被规约为 0/S,终点同理可以规约到 0/T。于是考虑设状态 fi,x,y,s 为考虑前 i 位(0,1..,i 位),起点是 S/0,终点是 T/2i+1,选择奇数次异或操作的集合是 s,需要最小的操作数是多少。接下来考虑 ii+1 的转移。

  • 已经满足条件,不需要操作:此时需要满足 statesStartx=Endy,直接 fi,x,y,sfi+1,x,y,s

  • 由若干个阶段拼起来:首先从 x 进行操作到 2i+2,接着通过若干次进位,最后一个阶段从 2i+2y。注意因为我们的进位至多进行一次,因此每次中间的进位操作一定要满足 statesi+1 位是 1。具体转移是 fi,x,1,s1+fi,1,1,s2+...+fi,1,1,sss1+fi,1,y,sssfi+1,x,y,s

这个东西就是一个最短路的形式,dijkstra 优化即可。

qwq
#include<bits/stdc++.h>
#define ll long long
#define int long long
using namespace std;
const int MAXV = (1ll << 8) + 10, N = 10;
const ll INF = 1e18;
void chkmin(ll& x, ll y){if(y < x) x = y;}
bool ex(int s, int i){return (s & (1ll << i));}
int n, a[N], S, T, st[MAXV], qwq[4];
bool vis[MAXV];
ll c[N], f[2][2][MAXV], g[2][2][MAXV], A, w[MAXV], dis[MAXV];
void upd(int bt, int x){
//cerr << bt << "\n";
for(int s = 0; s < (1 << n); s++){
vis[s] = 0;
if(ex(st[s], bt) == qwq[x]) dis[s] = g[x][1][s];
else dis[s] = INF;
}
for(int rnd = 1; rnd <= (1 << n); rnd++){
int p = -1;
for(int s = 0; s < (1 << n); s++) if((p == -1 || dis[p] > dis[s]) && (!vis[s])) p = s;
if(p == -1 || dis[p] >= INF) return;
vis[p] = 1; //cerr << p << " " << dis[p] << "\n";
for(int s = 0; s < (1 << n); s++){
if(vis[s] || (!ex(st[p ^ s], bt))) continue;
chkmin(dis[s], dis[p] + g[1][1][s ^ p]);
}
for(int s = 0; s < (1ll << n); s++){
for(int y = 0; y < 2; y++) if(qwq[2 + y] == (ex(st[s], bt) ^ 1)) chkmin(f[x][y][s ^ p], dis[p] + g[1][y][s]);
}
}
}
signed main(){
ios::sync_with_stdio(0);
cin.tie(0); cout.tie(0);
cin >> n >> S >> T >> A; qwq[1] = 0; qwq[3] = 1;
for(int i = 0; i < n; i++) cin >> a[i] >> c[i];
for(int s = 0; s < (1 << n); s++)
for(int i = 0; i < n; i++)
if(ex(s, i)) w[s] += c[i], st[s] ^= a[i];
for(int x = 0; x < 2; x++)
for(int y = 0; y < 2; y++)
for(int s = 0; s < (1 << n); s++) f[x][y][s] = w[s] + y * A;
for(int i = 0; i < 40; i++){
qwq[0] = ex(S, i); qwq[2] = ex(T, i);
for(int x = 0; x < 2; x++)
for(int y = 0; y < 2; y++)
for(int s = 0; s < (1 << n); s++){
//cerr << x << " " << y << " " << s << " " << f[x][y][s] << "\n";
g[x][y][s] = f[x][y][s];
if((qwq[x] ^ ex(st[s], i)) != qwq[2 + y]) f[x][y][s] = INF;
}
for(int x = 0; x < 2; x++) upd(i, x);
}
ll ans = INF;
for(int s = 0; s < (1 << n); s++) chkmin(ans, f[0][0][s]);
if(ans >= INF){cout << -1; return 0;}
cout << ans;
return 0;
}

AGC052C Nondivisible Prefix Sums

首先由于可以直接重排,顺序是不重要的,考虑一个一个将这些数加入序列中。假设现在已经确定了一个前缀信息,考虑接下来放什么数,显然只会有一种数是不合法的,为了保持合法,那么一个很 naive 的想法就是保持手上有至少 2 种数。于是当序列中没有绝对众数时就是合法的。但有绝对众数就不合法吗?显然不是。

接下来,为了方便分析,不妨让所有的数都乘上众数的逆元,这样众数就是 1 了。这一步非常神仙啊。设其他的数构成序列 b,显然我们尽量放 1 更好,所以我们让 a1,a2...,ap1 都是 1,那么 ap 就不能是 1 了,就随便放一个 bi 即可。那么 ap+1,...,ap+pbi1 都可以放 1 了。于是最多放 1 的个数就是 ipbi+p1,所以这个是必要条件。再考虑是否充分,唯一的问题就是假设 1 用完后剩下有 bi 出现绝对众数的怎么办。那在 1 不再是剩下的数的众数时,不难发现此时已经没有绝对众数了,因此合法了。

于是一个序列合法的充要条件就是:ncnti=1cntpbi

直接算合法的其实不好做,于是考虑算不合法的。那么后面的东西不能超过 n,直接 DP 加前缀和优化即可。

最后只剩总方案数了,但是注意上面的东西都没有考虑到一个东西,就是总共和等于 p 的倍数的情况。这个在写一个递推即可。

qwq
#include<bits/stdc++.h>
#define ll long long
using namespace std;
const int N = 5000 + 10;
const ll mod = 998244353;
int n;
ll P, jc[N], jcinv[N], f[N], g[N], h[N], sum[N];
void ADD(ll& x, ll y){x += y; (x >= mod) ? x -= mod : 0;}
ll qpow(ll x, int y){
ll ret = 1;
for(; y; y >>= 1, x = x * x % mod) if(y & 1) ret = ret * x % mod;
return ret;
}
ll C(int x, int y){return jc[x] * jcinv[x - y] % mod * jcinv[y] % mod;}
signed main(){
ios::sync_with_stdio(0);
cin.tie(0); cout.tie(0);
cin >> n >> P; jc[0] = jcinv[0] = 1;
for(int i = 1; i <= n; i++) jc[i] = jc[i - 1] * i % mod, jcinv[i] = qpow(jc[i], mod - 2);
g[0] = 1;
for(int i = 1; i <= n; i++) g[i] = (P - 1) * h[i - 1] % mod, h[i] = (g[i - 1] + h[i - 1] * (P - 2) % mod) % mod;
ll ans = (P - 1) * h[n] % mod; sum[0] = 1;
if(n % P && P <= n) ADD(ans, mod - (P - 1));
for(int i = 1; i <= n; i++){
for(int j = 1; j <= n; j++) sum[j] = (f[j] + sum[j - 1]) % mod;
for(int j = 1; j <= n; j++){
f[j] = (sum[j - 1] + mod - ((j - (P - 1) >= 0) ? sum[j - P + 1] : 0ll)) % mod;
if((n - i - j) % P != 0 && n - i > j + P - 1) ADD(ans, mod - f[j] * C(n, i) % mod * (P - 1) % mod);
} sum[0] = 0;
} cout << ans;
return 0;
}

AGC040E Prefix Suffix Addition

非常好 DP 题。

a 序列的产生可以看做前后缀分别操作最后累加导致的,分开考虑,先考虑只有前缀操作有效的情况可能生成什么样的序列。不难发现,这可以由一些升序序列拼接组成。同样的,只有后缀操作产生的序列只有可能是由一些降序序列拼成。

尝试使用 DP,设 fi,j 为考虑前 i 个数,前缀在第 i 个数上加了 bi=j 次,转移如下:

fi,j=mink=0aifi1,k+[j>k]+[aij<ai1+k]

不难发现 fi 是单调不降的,且 fi 有很多值相同的连续段,具体如何呢?打表可以发现,满足 x,fi,xfi,02 。这是因为假设 fi,0 的最优决策是 k,那么 fi,xfi1,k+2,于是进一步得 fi,xfi,0+2

然后直接维护连续段即可,因为我不想分讨所以最后写的是二分做法。

qwq
#include<bits/stdc++.h>
#define ll long long
#define pb emplace_back
using namespace std;
const int N = 2e5 + 10;
int n, a[N];
struct node{
int l, r, val;
};
vector<node> f[N];
void chkmin(int& x, int y){if(y < x) x = y;}
int get(int i, int pos){
int ans = 1e9;
for(auto qwq : f[i - 1]) chkmin(ans, qwq.val + (pos < qwq.l) + (a[i - 1] - qwq.l < a[i] - pos));
return ans;
}
signed main(){
ios::sync_with_stdio(0);
cin.tie(0); cout.tie(0);
cin >> n;
for(int i = 1; i <= n; i++) cin >> a[i];
f[0].pb((node){0, 0, 0});
for(int i = 1; i <= n + 1; i++){
for(int l = 0; l <= a[i];){
int r = l, val = get(i, l);
int L = l, R = a[i], mid;
while(L <= R){
mid = (L + R >> 1);
if(get(i, mid) == val) L = mid + 1, r = mid;
else R = mid - 1;
} f[i].pb((node){l, r, val}); l = r + 1;
}
} cout << f[n + 1][0].val;
return 0;
}

本文作者:Little_corn

本文链接:https://www.cnblogs.com/little-corn/p/18281742

版权声明:本作品采用知识共享署名-非商业性使用-禁止演绎 2.5 中国大陆许可协议进行许可。

posted @   Little_corn  阅读(6)  评论(0编辑  收藏  举报
点击右上角即可分享
微信分享提示
评论
收藏
关注
推荐
深色
回顶
收起