a 线性DP

AT_abc419_e [ABC419E] Subarray Sum Divisibility - 洛谷

数论同余 + DP

一、题目核心理解
给定长度为 n 的序列 a,可对任意元素执行加 1 操作(次数不限),要求所有长度为 L 的连续子段和都是 m 的倍数,求最少操作次数。
二、核心转化思路(题解关键)

  1. 连续段和的约束转化
    对于两个相邻的长度为 L 的连续段:
  • 第 1 段:a₁+a₂+…+a_L(记为 sum₁)
  • 第 2 段:a₂+a₃+…+a_{L+1}(记为 sum₂)
    要求 sum₁ % m = 0 且 sum₂ % m = 0,则 sum₂ - sum₁ = a_{L+1} - a₁ 必须是 m 的倍数,即 a₁ ≡ a_{L+1} (mod m)。
    推广结论:所有下标满足 x ≡ i (mod L) 的元素(第 i 组,i=1~L)必须同余(对 m 取模结果相同)。
  1. 代价预处理
    对第 i 组元素,若统一调整为模 m 余 f,则每个元素需要的操作次数为 (f - a[x] + m) % m(加 m 是避免负数),所有元素的操作次数之和记为 s[i][f](第 i 组调整为余 f 的总代价)。
  2. DP 求解最终约束
    前 L 组的余数之和必须是 m 的倍数(因为第 1 个长度为 L 的段和是前 L 组各选一个余数的和),用 DP 求满足该约束的最小总代价。
    ![[c43dfaf34866240a0e298104504117b0.png]]
const int N = 5e2 + 5;
//我不行了..DPPP难想难写。。
int dp[N][N], s[N][N];
void solve(){
  int n, m, L; cin >> n >> m >> L; vi a(n + 1);
  per(i, 1, n)cin >> a[i];
  //预处理
  per(st, 1, L)//第st组内统一调整为模m余f的总代价
    per(f, 0, m - 1)
      for(int i = st; i <= n; i += L)
        s[st][f] += (f - a[i] + m) % m;
  memset(dp, 0x3f, sizeof dp); dp[0][0] = 0;
  per(i, 1, L)//对于每个组进行DP,看选哪个余数总操作数最小
    per(j, 0, m - 1)//但是第i组的余数要由第i - 1组转移过来,因为第i和i - 1组中元素在a[i]中是相邻的
      per(k, 0, m - 1)
        dp[i][k] = min(dp[i][k], dp[i - 1][j] + s[i][(k - j + m) % m]);//将余数由j变为k的代价
        //dp[i][k] = min(dp[i][(j + K) % m], dp[i - 1][j] + s[i][k] % m);//将余数由j变为j + k的代价
  cout << dp[L][0] << endl;
}

P1108 低价购买 - 洛谷

最长下降子序列 + 方案数去重

点击查看解析 在“最长下降子序列(LDS)的固定长度”下,若两个元素的“结尾价格相同”,则它们对应的方案必然是重复的。dp[i](固定长度)已经把“能构成最优方案的前序路径范围”锁死了,不是任意路径都能参与i的方案计数,只有符合“长度-1 + 价格更大”的路径才有效。如果仅是长度相同但是结尾数不同,相关方案也会参与计数,并被后面的元素累加。 示例:序列 8 7 5 6 4 5 7 针对 i=6(a=5,dp=3)的计算过程(重点关注去重逻辑): 1. 初始状态:f[6] = 0(因 dp[6]≠1,不满足初始方案数为1的条件); 2. 遍历 j 从 1 到 5(j < i): j=1:dp[6]=3 ≠ dp[1]+1(2)→ 无贡献; j=2:dp[6]=3 = dp[2]+1(2+1)且 a[6]=5 < 7 → f[6] += f[2] = 1(暂为 1); j=3:dp[6]=3 = dp[3](3)且 a[6]=5 = a[3](5)→ 触发去重,f[6] = 0(关键!); 后续 j=4、5:即使满足转移条件,f[6] 已为 0,无影响; 3. 由于 dp[6]=3 ≠ ans1(最大 LDS 长度 4),因此 f[6] 不参与最终答案 ans2 的累加; 4. 最终结果:f[6]=0(去重成功,作废该方案)。
void solve(){
	int n; cin >> n;
	vi a(n + 1), dp(n + 1), f(n + 1);
	for(int i = 1; i <= n; ++i)cin >> a[i];
	int ans1 = 0, ans2 = 0;
	for(int i = 1; i <= n; ++i){
		dp[i] = 1;
		for(int j = 1; j <i; ++j)
			if(a[i] < a[j])dp[i] = max(dp[i], dp[j] + 1);
		ans1 = max(ans1, dp[i]);
	}
	for(int i = 1; i <= n; ++i){
		if(dp[i] == 1) f[i] = 1;
		for(int j = 1; j < i; ++j)
			if(dp[i] == dp[j] + 1 && a[i] < a[j]) f[i] += f[j];
			else if(dp[i] == dp[j] && a[i] == a[j]) f[i] = 0;
    if(dp[i] == ans1) ans2 += f[i];
  }
  cout << ans1 << " " << ans2 << endl;
}

P2347 [NOIP 1996 提高组] 砝码称重 - 洛谷

//多重背包写法
const int N = 1e4 + 7;
int cnt[7], ans;
int w[7] = {0, 1, 2, 3, 5, 10, 20};
bool f[N];//第i个砝码用j个在1~1000能表示的重量的个数
void solve(){
    f[0] = 1;
    per(i, 1, 6) cin >> cnt[i];
    per(i, 1, 6) per(j, 1, cnt[i])
      rep(k, 1000, 0)
       if(f[k])//防止砝码复用
        f[k + w[i]] = 1;
    per(i, 1, 1000)ans += f[i];
    cout << "Total=" << ans << endl;
}
//多重背包改01背包 && 01背包方案数写法
//当前方案只能从有方案的情况转移来
int ans;
int w[7] = {0, 1, 2, 3, 5, 10, 20};
bool f[N];
void solve(){
    f[0] = 1; vi p; p.push_back(0);
    int x, V = 0;
    per(i, 1, 6) {
        cin >> x; 
        per(j, 1, x)p.push_back(w[i]), V += w[i];
    }
    for(auto v: p)
        rep(j, V, v)f[j] += f[j - v];
    per(i, 1, V)ans += f[i];
    cout << "Total=" << ans << endl;
}

bitset<>算法

操作 功能说明 代码中用途
bitset<N>bt; 定义 N 位的位集,默认所有位为 0 s[0] = 1 初始化 “重量 0 有效”
bt[pos] 访问第 pos 位(0 起始索引),可读写 s[0] = 1 标记初始状态
bt << k 位集整体左移 k 位,低位补 0 s << w[i] 等价于 “所有有效重量 + k”
| bt 位的或运算 按位或运算,合并两个位集的 “有效状态” 合并 “原重量” 和 “加新砝码后的重量”
bt.count() 统计位集中 1 的个数(有效状态总数) 计算 “可称量的所有重量数”
具体见STL中详细列出
bitset<1001>bt;
int w[7] = {0, 1, 2, 3, 5, 10, 20};
void solve(){
    int m; bt[0] = 1;//第0个位置赋值为1
    per(i, 1, 6){
        cin >> m;
        per(j, 1, m)bt |= bt << w[i]; 
    }
    cout << "Total=" << bt.count() - 1 << endl;//下标从1开始
}

CF2000F Color Rows and Columns - 洛谷

贪心 + DP

状态定义
dp[i][j]表示前i个矩形获得j分的最小操作次数(代价)。
转移方程
对于第i个矩形,若在该矩形上获得k分(0 ≤ k ≤ j),则前i-1个矩形需获得j-k分。因此:

dp[i][j] = min(dp[i][j], dp[i-1][j-k] + f(i, k))

其中f(i, k)表示第i个矩形获得k分的最小操作次数(需预处理)。
关键:计算f(i, k)(单个矩形得k分的代价)
一个矩形要获得k分,需通过着色行或列来实现。最优策略是每次选择当前较短的维度(行或列)着色

#include<bits/stdc++.h>
using namespace std;
#define endl '\n'
//#define int long long
#define vi vector<int>
#define vii vector<vi>
#define for(i, a, b) for(int i = (a); i <= (b); ++i)
#define rof(i, a, b) for(int i = (a); i >= (b); --i)
const int inf = 0x3f3f3f3f;
void solve(){
  int n, m; cin >> n >> m;
  vi a(n + 1), b(n + 1);
  vii dp(n + 1, vi(m + 1, inf));//前i个矩形得到j分的最小代价
  dp[0][0] = 0;
  for(i, 1, n)cin >> a[i] >> b[i];
  for(i, 1, n){
    int res = 0, x = a[i], y = b[i];
    for(k, 0, a[i] + b[i]){//枚举在当前矩形可能获得的分数k
      for(j, k, m) dp[i][j] = min(dp[i][j], dp[i - 1][j - k] + res);
      //更新DP: 前i个矩形得j分 = min(前i-1个矩形得j-k分 + 当前矩形得k分的代价)
      //类比背包
        if(x >= y)res += y, --x;//枚举边,选择最小的那边
        else res += x, --y;
    }
  }
  cout << (dp[n][m] == inf ? -1 : dp[n][m]) << endl;
}
signed main() {
    ios :: sync_with_stdio(0), cin.tie(0), cout.tie(0);
    int T = 1;
    cin >> T; 
    while (T--) solve();
    return 0;
}

P1103 书本整理 - 洛谷

#include<bits/stdc++.h>
using namespace std;
#define endl '\n'
const int N = 1e2 + 7;
int f[N][N];//总共选取n-k本书,前i本书中选取j本书(且包含第j本)的最小高度差。
struct node{
  int h, w;
  bool operator < (const node &x)const{
    return x.h > h; 
  }
}a[N];
void solve(){
  int n, K;
  cin >> n >> K;
  int m = n - K;
  memset(f, 0x3f, sizeof f);
  for(int i = 1; i <= n; ++i)
    cin >> a[i].h >> a[i].w;
  sort(a + 1, a + 1 + n);
  for(int i = 0; i <= n; ++i)f[i][1] = 0;
  for(int i = 2; i <= n; ++i)
    for(int j = 1; j < i; ++j)
      for(int k = 2; k <= (i, m); ++k)//对选取的书进行枚举
      f[i][k] = min(f[i][k], f[j][k - 1] + abs(a[i].w - a[j].w));
      //(f[i][k], )用于每轮的更新比较存状态
      //前i本书选取k个的最小不整齐度 = 前j本书中选取k−1本书的最小不整齐度 + 当前这本书与第j书的宽度差(也就是说从j+1到i−1的书都不选取)
 int ans = 0x3f3f3f3f; 
  for(int i = m; i <= n; ++i)ans = min(ans, f[i][m]);
 cout << ans << endl;
}
signed main(){
  ios :: sync_with_stdio(0), cin.tie(0), cout.tie(0);
  int T = 1;
  //cin >> T;
  while(T--)solve();
  return 0;
}
点击查看解析 我们可以设计一个状态fij​表示前i个数中去掉j个的最小不整齐度,仔细思考一下,我们在去掉一本书时,它改变的不整齐度为前一本书和后一本本的宽度差和它与两本书的宽度差,前一本书我们不能保证是哪一本书,我们只能用枚举来表示,但后一本书呢,我们发现无法处理了,所以这样做貌似不行; 我们不妨反过来思考,在n本书中去掉k本书也就是相当于在n本书中选取n−k本书,所以我们的状态fij​可以表示为前i本书中选取j本的最小不整齐度,在转移时我们只需要关注前一本书是哪一本,而后一本书是不需要我们考虑的,所以转移方程也就显而易见了: ``` f[i][l]=min(f[i][l],f[j][l−1]+abs(a[i].width−a[j].width)); ``` i表示枚举到了第几本书,l表示当前已经选取了几本书 意思为前i本书选取l个的最小不整齐度为前j本书中选取l−1本书的最小不整齐度+当前这本书与第j书的宽度差(也就是说从j+1到i−1的书都不选取)。
# [P1025 [NOIP 2001 提高组] 数的划分 - 洛谷](https://www.luogu.com.cn/problem/P1025)
点击查看解析

为什么要分有1的和没1的?
核心原因是:通过 “是否含 1” 拆分,能让两种情况都满足 “无序划分”,且不重复、不遗漏地覆盖所有可能,最终把大问题拆成可递推的小问题 —— 这是整数划分 DP 的关键技巧。
先想:无序划分的核心痛点是什么?
无序划分要求1+2和2+1算同一种,本质是要 “避免顺序干扰”。如果直接枚举 “第 i 份选什么数”,很容易重复计数。
而 “是否含 1” 是一个天然的 “互斥拆分标准”:任何一个无序划分,要么有 1,要么全是≥2 的数,没有第三种可能。用这个标准拆分,能保证:

  1. 两种情况不重叠(一个划分不可能既有 1 又没 1);
  2. 两种情况能覆盖所有可能(所有划分都逃不开这两类)。
    再想:拆分后为什么能递推?
    拆分不是目的,目的是把当前状态f[i][j](j 分成 i 份)转化成更小的、已经算过的子状态,避免重复计算。
  3. 含 1 的情况 → 转化为f[i-1][j-1]
    如果划分里有至少一个 1,我们可以把这个 1 “单独拎出来”—— 剩下的就是 “把 j-1 分成 i-1 份”(因为拿走了 1 个 1,总和减 1,份数减 1)。
  • 比如:把 7 分成 3 份(含 1)→ 拎出一个 1,剩下的就是 “把 6 分成 2 份”(1+5、2+4、3+3),加回 1 就是原来的划分(1+1+5、1+2+4、1+3+3)。
  • 关键:子状态f[i-1][j-1]本身也是无序划分,所以不会引入重复。
  1. 不含 1 的情况 → 转化为f[i][j-i]
    如果划分里全是≥2 的数,我们给每个数都减 1(比如 2→1、3→2)—— 这样所有数都变成≥1 的正整数,总和减少 i(i 个数各减 1),份数不变,就转化成了 “把 j-i 分成 i 份”。
  • 比如:把 7 分成 3 份(全≥2)→ 各减 1 后变成 “把 4 分成 3 份”(1+1+2),加回 1 就是原来的划分(2+2+3)。
  • 关键:减 1 后依然是无序划分,且避免了和 “含 1 情况” 的重叠。
    总结:这是最简洁的递推路径
    如果不按 “是否含 1” 拆分,比如直接枚举 “第 i 份选 k(k≥1)”,会出现两个问题:
  1. 重复计数(比如选 k=1 和 k=2,会出现1+2和2+1的不同路径);
  2. 子状态无法直接复用(枚举 k 后,很难保证无序性)。
    而 “是否含 1” 的拆分,刚好解决了这两个问题:
  • 不用考虑顺序,因为两种情况的递推都基于 “无序子状态”;
  • 子状态f[i-1][j-1]和f[i][j-i]都比当前状态小,能从基础状态(比如f[0][0]=1、f[1][j]=1)逐步推导出来。
    简单说:这是整数划分问题中,“唯一能同时满足无序、不重复、可递推” 的拆分方式,是前人总结的高效技巧~
### 划分DP常用技巧(分为有1的和无1的) ![[75dfa7166d35d0a460d83090f1a1e631.png]] ```cpp #include using namespace std; #define endl '\n' const int N = 205; int n, k; int f[N][N]; // f[i][j]:将j分成i份的无序划分方法数 void solve(){ cin >> n >> k; // 初始化:分成1份的情况 for(int j = 1; j <= n; ++j)f[1][j] = 1; for(int i = 2; i <= k; ++i) // 枚举份数(从2到k) for(int j = i; j <= n; ++j) // 枚举总和(至少为i,因为每份≥1) // 转移方程:两种情况之和 f[i][j] = f[i-1][j-1] + f[i][j - i];//两种情况都是由之前推来的 cout << f[k][n] << endl; } signed main(){ ios :: sync_with_stdio(0), cin.tie(0), cout.tie(0); int T = 1; // cin >> T; while(T--) solve(); return 0; } ```

E - Hungry Takahashi

二分+DP

#include<bits/stdc++.h>
using namespace std;
#define endl '\n'
#define int long long
#define vi vector<int>
#define vii vector<vi>
const int N = 5e5 + 7, inf = 0xc0c0c0c0c0c0c0c0;
void solve(){
    int h, w; cin >> h >> w;
    vii a(h + 1, vi(w + 1));
    vector<int>p(h + w);
    for(int i = 1; i <= h; ++i)
        for(int j = 1; j <= w; ++j)cin >> a[i][j];
    for(int i = 1; i < h + w; ++i)cin >> p[i];
    auto check = [&](int mid) ->bool{
        vii dp(h + 1, vi(w + 1, inf));
        dp[1][1] = mid + a[1][1] - p[1];
        if(dp[1][1] < 0) return 0;  // 起点就小于0,直接返回
        for(int i = 1; i <= h; ++i)
            for(int j = 1; j <= w; ++j){
                if(i == 1 && j == 1)continue;
                dp[i][j] = max(dp[i - 1][j], dp[i][j - 1]);
                if(dp[i][j] == inf)continue;//不可达去看下一条路
                dp[i][j] += a[i][j] - p[i + j - 1];
                if(dp[i][j] < 0)dp[i][j] = inf;//标记为不可达
            }
        return dp[h][w] != inf;
    };//lamda结尾要有分号
    int l = 0, r = 1e18;
    while(l < r){
        int mid = l + r >> 1;
        if(check(mid))r = mid;
        else l = mid + 1;
    }
    cout << l << endl;
}
signed main(){
    ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
    int T = 1;
   // cin >> T; 
    while(T --)solve();
    return 0;
}

DP做法(从后往前推)

#include<bits/stdc++.h>
using namespace std;
#define endl '\n'
#define int long long
#define vi vector<int>
#define vii vector<vi>
const int N = 5e5 + 7, INF = 0x3f3f3f3f3f3f3f3f;
//设 dpi,j​ 表示从 (i,j) 出发走到终点需要的最少初始硬币数。所以要从终点往前递推
//且从前往后推无法保证 “剩余硬币满足下一个格子的需求”。

//因为后面的值是有效的,前面的值不一定(走不走)所以要从后往前推
//从前往后推找最大要初始化最小,从后往前推找最小要初始化最大
void solve(){
    int h, w; cin >> h >> w;
    vii a(h + 2, vi(w + 2)), dp(h + 2, vi(w + 2, INF));//到达格子 (i,j) 时,必须持有的最小初始硬币数
    vector<int>p(h + w);
    for(int i = 1; i <= h; ++i)
        for(int j = 1; j <= w; ++j)cin >> a[i][j];
    for(int i = 1; i < h + w; ++i)cin >> p[i];
    dp[h][w] = p[h + w -1] - a[h][w];
    for(int i = h; i; --i)
        for(int j = w; j; --j){
            if(i == h && j == w)continue;
            if(i < h)dp[i][j] = min({dp[i][j], max(0ll, dp[i + 1][j] + p[i + j - 1] - a[i][j])});//还需的钱数
            if(j < w)dp[i][j] = min({dp[i][j], max(0ll, dp[i][j + 1] + p[i + j - 1] - a[i][j])});
        }
    cout << max(0ll, dp[1][1]) << endl;
}
signed main(){
    ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
    int T = 1;
   // cin >> T; 
    while(T --)solve();
    return 0;
}

D - XNOR Operation

DP

#include<bits/stdc++.h>
using namespace std;
#define endl '\n'
#define int long long
#define vi vector<int>
const int N = 2e5 + 7;
//字串合并只有两种情况,即前面合并后的结果为0,或1
//把前面的两种状态储存起来
int dp[N][2];
//0: 第i个位置合并成0结尾的1最大值;
//1:第i个位置合并成1结尾的1最大值;
void solve() {
    int n, ans = 0; cin >> n;
    char ch;
    for(int i = 1; i <= n; ++i){
        cin >> ch;
        if(ch == '1'){
            dp[i][1] = dp[i - 1][1];
            ++dp[i][1];
            dp[i][0] = dp[i - 1][0];
        }
        else{//ch == '0
            dp[i][0] = dp[i - 1][1];
            ++dp[i][0];
            dp[i][1] = dp[i - 1][0];
        }
    }
    for(int i = 1; i <= n; ++i)ans += dp[i][1];
    //注意总数是每个字串合能并成1的个数和
    cout << ans << endl;
}
signed main() {
    ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
    int T = 1;
    //cin >> T;
    while (T--) solve();
    return 0;
}

分治 + 前缀和 + 区间

同或和异或都有结合律和传递性
这个同或相当于把1吸收了,要重点关注0的个数

#include<bits/stdc++.h>
using namespace std;
#define endl '\n'
#define int long long
#define vi vector<int>
const int N = 2e5 + 7;
//如果一个区间有奇数个0,则该区间的合并结果为0
//如果一个区间有偶数个0 ,则该区间的合并结果为1
//统计前缀中0的个数的奇偶
//因为 偶 = 奇 - 奇,偶 = 偶 - 偶
void solve() {
    int n, cnt0 = 0, ans = 0; cin >> n;
    char ch; vi b(2); b[0] = 1;
    for(int i = 1; i <= n; ++i){
        cin >> ch;
        cnt0 += (ch == '0');
        ans += b[cnt0 % 2];  ++b[cnt0 % 2];
        //如果当前前缀有奇数个0的话,减去前面有奇数个0的前缀便可得偶数个0的区间
    }
    cout << ans << endl;
}
signed main() {
    ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
    int T = 1;
    //cin >> T;
    while (T--) solve();
    return 0;
}

D - Takahashi's Expectation

分治 + DP + 二分

#include<bits/stdc++.h>
using namespace std;
#define endl '\n'
//#define int long long
#define vi vector<int>
#define vii vector<vi>
const int N = 1e3 + 5;
//可以利用dp枚举1~1000内初始值为1~1000的所有情况(类似于打表)
//大于1000一直减,小于1000会一直小于1000
//为什么要以1000为分界线?因为减到1000后,后面的初始值都不会超过1000, 所以只要枚举1000就好了
//dp[i][j]表示从第i个礼物开始到最后的心情值为j,又是知道最后的,所以从后往前推看为j的能到达哪些状态
//dp[n+1][j] = j 表示:如果没有礼物需要处理,那么最终心情值就是当前的心情值 j。
void solve() {
    int n; cin >> n;
    vi p(n + 1), a(n + 1), b(n + 1), smb(n + 1);
    vii dp(n + 2,  vi(N));
    for(int i = 1; i <= n; ++i)cin >> p[i] >> a[i] >> b[i], smb[i] = smb[i - 1] + b[i];
    for(int j = 0; j <= 1000; ++j)dp[n + 1][j] = j;
    for(int i = n; i; --i)//因为一般是从中间某个点开始变为1000的,一直到最后,所以要从后往前遍历,保证后面都被取到
        for(int j = 0; j <= 1000; ++j)//每个i都能由已知的(i + 1)转移过来
            dp[i][j] = dp[i + 1][j <= p[i] ? j + a[i] : max(0, j - b[i])]; 
    int q, x; cin >> q;
    while(q --){
        cin >> x;
        if(x - 500 > smb[n]){cout << x - smb[n] << endl; continue;}//保证减下来的全程大于1000
        if(x <= 1000){cout << dp[1][x] << endl; continue;}//既然dp到1000了就利用吧
        int id = upper_bound(smb.begin() + 1, smb.end(), x - 500) - smb.begin();//一直减到只剩500(或1000都可以)
        cout << dp[id + 1][x - smb[id]] << endl;//这个位置是 >= 500的,直接从下一个位置开始(sum[id + 1 - 1])
    }
}
signed main() {
    ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
    int T = 1;
    //cin >> T;
    while (T--) solve();
    return 0;
}

D - Forbidden Difference

#include<bits/stdc++.h>
using namespace std;
#define endl '\n'
const int N = 2e6 + 5;//2*N!!
//哎,如果情况太多难以决策或贪心不出来就用DP来考虑所有情况吧
int dp[N][2];//对于数字i, 0删前,1删自
int b[N];
void solve(){
    int n, k, x, mx = 0, mn = N, ans = 0; cin >> n >> k;
    for(int i = 1; i <= n; ++i)cin >> x, ++b[x], mn = min(mn, x), mx = max(mx, x);
    if(!k){//要特判!!
        for(int i = mn; i <= mx; ++i)ans += (b[i]? b[i] - 1 : 0);
        cout << ans << endl;
        return ;
    }
    for(int i = mn; i < mn + k; ++i)dp[i][1] = b[i];//初始化
    for(int i = mn + k; i <= mx; ++i){
        dp[i][1] = min(dp[i - k][0], dp[i - k][1]) + b[i],
        dp[i][0] = min(dp[i - k][0] + b[i - k], dp[i - k][1]);//dp[i - k][1]表示i - k是就已经删自了,到i时就已经不在了
    }
   //有的最终不是正好转移到mx, 而是落在[mx - k + 1, mx]区间
   //因为对于有点数来说 +k 的终点不是mx,而是mx的前面的一些数
   //所欲对于这种差值转移过来的一定要记得枚举最后一个区间
   //枚举所有的最终状态
   for(int i = max(0, mx - k + 1); i <= mx; ++i)ans += min(dp[i][0], dp[i][1]);
   cout << ans << endl;
}
signed main() {
    ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
    int T = 1;
    //cin >> T;
    while(T--) solve();
    return 0;
}

E - Bowls and Beans

贪心 + DP

资源收集 / 转移问题/范围约束转移

贪心:一定是转移到前面最远的有豌豆的碗最优,如果前面的碗中都没有豌豆就找前面没豆碗中能到达最优碗的最小步数+1, 所以无论有豆没豆都要记录过程状态
dp表示能到达上一个有豆碗中的最小步数,对于每个有豆的碗记录其过程步数

#include<bits/stdc++.h>
using namespace std;
#define endl '\n'
#define vi vector<int>
const int inf = 0x3f3f3f3f;
const int N = 2e3 + 5;// !
int dp[N];
void solve(){
    memset(dp, 0x3f, sizeof dp);
    int n, ans = 0; cin >> n;
    vi a(n + 1), c(n + 1);
    a[0] = 1, dp[0] = 0;
    for(int i = 1; i <= n - 1; ++i)cin >> c[i];
    for(int i = 1; i <= n - 1; ++i)cin >> a[i];
    for(int i = 1; i <= n - 1; ++i){
        for(int j = i - c[i]; j <= i - 1; ++j){
            dp[i] = min(dp[i], dp[j] + 1);
            if(a[j]){dp[i] = 1; break;} 
        }
        if(a[i])ans += dp[i];
    }
    cout << ans << endl;
}
signed main() {
    ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
    int T = 1;
    //cin >> T;
    while(T--) solve();
    return 0;
}

E. Three Strings

字符串匹配问题

#include<bits/stdc++.h>
using namespace std;
#define endl '\n'
#define vi vector<int>
#define vii vector<vi>
const int N = 3e3 + 5;
//int f[N][N];// 表示使用 a 的前 i 个字符和 b 的前 j 个字符构造出 c 的前 i+j 个字符时,最少需要改变的字符数量。
//对于a中前i个个字符看把它插在哪里合适、对于b中前j个个字符看把它插在哪里合适, 递推能都按照原a、b中的相对位置插入递推上去
//为什么不重不漏?因为当前的i + j 必须是从前面已经配好的i + j - 1继承 + 当前配对情况转移过来的,无论是用a配还是用b配当前都是已经使用了(i + j)个,如用a配就要由a[i - 1]和b[j]转移过来 + 当前a和c配的情况,每次最优可推全局最优
/*  
比如说原题中的例子caabdraa,当遍历到caab时我肯定时选择了b串中的ca和a串中的ab,这时是最优的*/
void solve(){
 string a, b, c;
 cin >> a >> b >> c;
 int n = a.size(), m = b.size();
 vii f(n + 1, vi(m + 1));
 for(int i = 0; i <= n; ++i){
    for(int j = 0; j <= m; ++j){
        if(i == 0 && j == 0)continue;
        else if(!i)f[i][j] = f[i][j - 1] + (b[j - 1] != c[j - 1]);
        else if(!j)f[i][j] = f[i - 1][j] + (a[i - 1] != c[i  -1]);
        else f[i][j] = min(f[i - 1][j] + (a[i - 1] != c[i - 1 + j]), f[i][j - 1] + (b[j - 1] != c[i + j - 1]));
    }
 }
 cout << f[n][m] << endl;//n和m是个数

}
signed main() {
    ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
    int T = 1;
    cin >> T;
    while(T--) solve();
    return 0;
}

CF455A Boredom - 洛谷

#include <bits/stdc++.h>
using namespace std;
#define endl '\n'
#define int long long  
#define vi vector<int>
const int N = 2e5 + 5;
int b[N], dp[N];
void solve() {
    int n, x, mx = 0; cin >> n;
    for(int i = 1; i <= n; ++i)cin >> x, ++ b[x], mx = max(mx, x);
    //因为x和x - 1, x + 1之间是连续的,所以我们只能记录最大值然后枚举每个数
    //若这个数没有则b[i]为0
    dp[1] = b[1] * 1;
    for(int i = 2; i <= mx; ++i)
        dp[i] = max(dp[i - 1], dp[i - 2] + b[i] * i);
    //dp[i - 1]表示当前数不取,即取i - 1
    //dp[i - 2]表示取当前数,则i - 1不取,值由i - 2转移过来
    //由于从前往后遍历就不用再关心i + 1了
    cout <<dp[mx] << endl;
}
signed main() {
    ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
    int T = 1;
    //cin >> T;
    while(T--) solve();
    return 0;
}

最长上升子序列(LIS)/最长不上升子序列问题算法详解+例题(树状数组/二分优化,看不懂你来打我)_牛客博客

LIA:

可惜了,要是该题是将已给序列恢复原序列,则可以用LCIS(最长连续递增(非单调)子序列)!
//理解写法 O(n) 容易超时
#include<bits/stdc++.h>
using namespace std;
#define endl '\n'
void solve(){
 int n; cin >> n;
 vector <int> a(n),b(n,1);
 for(auto &x: a) cin >> x;
 int mx=1;//LIA
 for(int i=1; i<n; ++i){
	for(int j=0; j<i; ++j)
	if(a[i]>a[j])b[i]=max(b[i],b[j]+1);
	mx = max(mx, b[i]);
 }
 cout << n-mx <<endl;;
 }
signed main(){
	ios::sync_with_stdio(0),cin.tie(0),cout.tie(0);
	int T = 1;
	cin >> T; 
	while( T-- ) solve();
	return 0;
}
优化:
#include<bits/stdc++.h>
using namespace std;
#define endl '\n'
void solve(){
 int n; cin >> n;
 vector <int> a(n+1), p(n+1);
 for(int i=1; i<=n; ++i) cin >> a[i], p[a[i]]=i;
 int mx=1, cur=1;//LIA
 for(int i=2; i<=n; ++i){
    if(p[i] > p[i-1]) ++cur, mx=max(mx,cur);
	else cur=1;
 }
 cout << n-mx <<endl;;
 }
signed main(){
	ios::sync_with_stdio(0),cin.tie(0),cout.tie(0);
	int T = 1;
	cin >> T; 
	while(T--) solve();
	return 0;
}

896. 最长上升子序列 II - AcWing题库

辨析:Problem - 847B - Codeforces
O(n^2)做法

#include<bits/stdc++.h>
using namespace std;
#define endl "\n"
//#define int long long
//#define ll long long
//vector<pair<int,int>>E[mxn];
//double ans=DBL_MAX;//double的最大值
//vector<vector<int>>e(mxn);
const int mxn=1e3+7;
int a[mxn],n,f[mxn],ans;
void solve(){
 cin>>n;
 for(int i=1; i <= n; ++i)cin >> a[i];
 for(int i = 1; i <= n; ++ i){
  f[i]=1;
  for(int j =1; j < i; ++ j){
    if(a[i] > a[j]) f[i] = max(f[i], f[j]+1);
  }
 }
  for(int i=1; i <= n; ++i)ans=max(ans, f[i]);
 cout << ans << endl;
}
signed main(){
  ios::sync_with_stdio(0),cin.tie(0),cout.tie(0);
   int T=1;
  //cin>>T;
  while(T--)solve();
  return 0;
}

O(n * logn)做法:

#include<bits/stdc++.h>
using namespace std;
#define endl "\n"
#define ll long long
const int N = 2e5 + 7;
const int inf=0xc0c0c0c0;
//LIS
int a[N],last[N]={inf},cnt;
vector<int>ans[N];
void solve(){
    int n,cnt=0; cin >> n;
   for(int i=1;i<=n;++i)cin>>a[i];
   for(int i=1;i<=n;++i){
    if(a[i]>last[cnt]){
      last[++cnt]=a[i];//维护最大上升子序列的最大值
     // ans[cnt].push_back(a[i]);
    }//大于直接插入,小于找到一个正好大于的地方更新
    else{
      int l=0,r=cnt;
      while(l<r){
        int mid=l+r>>1;
        if(a[i]<=last[mid])r=mid;//找到第一个LIS
        else l=mid+1;
      }
      //ans[l].push_back(a[i]);
      last[l]=a[i];//更新
    }
   }
//    for(int i=1;i<=cnt;++i){
//     for(auto &x:ans[i])cout<<x<<" ";
//    cout<<endl;
//    }
cout<<cnt<<endl;
}

signed main(){
    ios::sync_with_stdio(0),cin.tie(0),cout.tie(0);
    int T=1;
    //cin>>T;
    while(T--)solve();
    return 0;
}
错误的代码:

#include<bits/stdc++.h>
using namespace std;
#define endl "\n"
#define ll long long
const int N = 2e5 + 7;
const int inf=0xc0c0c0c0;
//LIS
int a[N],last[N]={inf},cnt;
//vector<int>ans[N];
void solve(){
    int n,cnt=0; cin >> n;
   for(int i=1;i<=n;++i)cin>>a[i];
   for(int i=1;i<=n;++i){
    if(a[i]>last[cnt]){
      last[++cnt]=a[i];
    }//大于直接插入,小于找到一个正好大于的地方更新
    else{
      int l=0,r=cnt;
      while(l<r){
        int mid=l+r>>1;
        if(last[mid]>=a[i])r=mid;
        else l=mid+1;
      }
      last[l]=a[i];//更新使此长度上升自序列的末尾值最小
    }
   }
   cout<<cnt<<endl;
   for(int i=1;i<=cnt;++i)cout<<last[i]<<" \n"[i==cnt];
}

signed main(){
    ios::sync_with_stdio(0),cin.tie(0),cout.tie(0);
    int T=1;
    //cin>>T;
    while(T--)solve();
    return 0;
}

last无法正确输出LIS

因为每次更新会把后面的最小值替换到前面去,也可以说last之间是独立的,它的任务是维持与cnt有关的最小值

i a[i] 操作 last 数组更新 cnt
1 3 放入空尾 last = [3] 1
2 10 10 > 3 last = [3, 10] 2
3 2 替换 3 last = [2, 10] 2
4 1 替换 2 last = [1, 10] 2
5 20 20 > 10 last = [1, 10, 20] 3

897. 最长公共子序列 - AcWing题库

#include<bits/stdc++.h>
using namespace std;
#define endl "\n"
//#define int long long
//#define ll long long
//#define ULL unsigned long long
//const int N=1e5+3,P=131;
const int N=1e3+10,M=N*31;
//ai,bj不相等的话会继承上一个其中一个的最大值;
//相等的话会在继承的最大值的基础上+1
//遍历两次即可找到最大值
int f[N][N];
char a[N],b[N];
void solve(){
   int n,m;
   cin>>n>>m>>a+1>>b+1;
   for(int i=1;i<=n;++i){
    for(int j=1;j<=m;++j){
        if(a[i]==b[j])f[i][j]=max(f[i][j],f[i-1][j-1]+1);
        else f[i][j]=max(f[i-1][j],f[i][j-1]);
    }
   }
    cout<<f[n][m]<<endl;
}  
signed main(){
    ios::sync_with_stdio(0),cin.tie(0),cout.tie(0);
    int T=1;
    //cin>>T;
   while(T--)solve();
   return 0;
}

P1435 [IOI 2000] 回文字串 - 洛谷

最长公共子序列

#include <bits/stdc++.h>
using namespace std;
#define endl '\n'
//#define int long long  
#define vi vector<int>
const int N = 1e3 + 5;
//正序和倒序的最长公共子序列
//因为找到最长公共子序列插入的字符数擦最少
int f[N][N];
void solve() {
  string s; cin >> s;
  string s1(s.rbegin(), s.rend());
  int n = s.size(); 
  s = ' ' + s; s1 = ' ' + s1;
  for(int i = 1; i <= n; ++ i){//s的前i和和s1的前j个的最长公共子序列
    for(int j = 1; j <= n; ++ j){
      if(s[i] == s1[j])f[i][j] = max(f[i][j], f[i - 1][j - 1] + 1);//要还是不要 
      else f[i][j] = max(f[i - 1][j], f[i][j - 1]);
    }
  }
  cout << n - f[n][n] << endl;
}
signed main() {
    ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
    int T = 1;
    //cin >> T;
    while(T--) solve();
    return 0;
}

902. 最短编辑距离 - AcWing题库

#include<bits/stdc++.h>
using namespace std;
#define endl "\n"
//#define int long long
#define ll long long
const int N = 1e3+7;
int f[N][N];
char a[N],b[N];
void solve(){
int n,m; cin>>n>>a+1>>m>>b+1;
for(int i=1;i<=n;++i)f[i][0]=i;
for(int i=1;i<=m;++i)f[0][i]=i;
for(int i=1;i<=n;++i){
    for(int j=1;j<=m;++j){
        if(a[i] == b[j])f[i][j] = f[i-1][j-1];  // 直接继承
        else f[i][j] = min({f[i-1][j], f[i][j-1], f[i-1][j-1]}) + 1;
    }
}
cout<<f[n][m]<<endl;
}
signed main(){
    ios::sync_with_stdio(0),cin.tie(0),cout.tie(0);
    int T=1;
    //cin>>T;
    while(T--)solve();
    return 0;
}

899. 编辑距离 - AcWing题库

#include<bits/stdc++.h>
using namespace std;
#define endl "\n"
//#define int long long
#define ll long long
const int N = 2e3+7;
int f[N][N];
string s[N];
int n;
int check(string s2,string s3){
    int la=s2.size(),lb=s3.size();
    s2=' '+s2; s3=' '+s3;
    for(int i=1;i<=la;++i)f[i][0]=i;
    for(int i=1;i<=lb;++i)f[0][i]=i;
    for(int i=1;i<=la;++i){
        for(int j=1;j<=lb;++j){
            if(s2[i]==s3[j])f[i][j]=f[i-1][j-1];
            else f[i][j]=min({f[i-1][j],f[i][j-1],f[i-1][j-1]})+1;//删除a[i],插入b[j],替换
        }
    }
return f[la][lb];
}
void solve(){
int n,m;
 cin>>n>>m;
 for(int i=1;i<=n;++i)cin>>s[i];
 while(m--){
    string s1;
    int li,res=0;
    cin>>s1>>li;
    for(int i=1;i<=n;++i)
        if(check(s[i],s1)<=li)++res;
         cout<<res<<endl;
 }
}
signed main(){
    ios::sync_with_stdio(0),cin.tie(0),cout.tie(0);
    int T=1;
    //cin>>T;
    while(T--)solve();
    return 0;
}

E - Battles in a Row

#include<bits/stdc++.h>
using namespace std;
#define endl '\n'
//#define int long long
#define ll long long
#define pii pair<int, int>
#define vi vector<int>
#define vii vector<vi>
#define lowbit(x) (x & (-x))
const int N = 3e3 + 5, inf = 0x3f3f3f3f;
int dp[N][N];//用最少的魔法和确定的生命值打败最多的怪物
void solve(){
  int n, h, w, ans = 0; cin >> n >> h >> w;
  vi a(n + 1), b(n + 1);
  memset(dp, 0x3f, sizeof dp);
  dp[0][0] = 0;
  for(int i = 1; i <= n;  ++i)cin >> a[i] >> b[i];
  for(int i = 1;  i <= n; ++i){//打败了第几个怪物
      for(int j = 0; j <= h; ++ j){//生命值
        dp[i][j] = min(dp[i][j], dp[i - 1][j] + b[i]);//用魔法打败
        if(j >= a[i])dp[i][j] = min(dp[i][j], dp[i - 1][j -a[i]]);//用生命值打败
        if(dp[i][j] <= w)ans = max(ans, i);
      }
  }
  cout << ans << endl;
}
signed main() {
    ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
    int T = 1;
    //cin >> T;
    while(T--) solve();
    return 0;
}

posted @ 2025-11-17 07:30  Glosie  阅读(15)  评论(0)    收藏  举报