跳转至

做题记录

字典树 做题记录

P4551

P4551

钦定根为 \(root\),我们首先考虑从 \(root\) 开始 \(dfs\) 处理出每个节点 \(i\) 到其的距离 \(dis[i]\);由于两节点的 \(LCA\) 部分对最终的异或答案不产生影响,有一个很显然的想法是枚举每对节点取最大值,但 \(O(n^2)\) 无法接受,需要效率更优的算法

已知节点 \(i\),不用枚举,如何算出其他节点到 \(i\) 的异或路径长度最大值?可以按位考虑 贪心;从高到低枚举每一位,我们希望新节点的当前位与 \(dis[i]\) 的当前位尽量不同;但是可能无法保证新节点的当前位总不同,即需要 验证不同的一位是否存在,这可以使用 \(01trie\) 维护

具体的,我们将每一个 \(dis[i]\) 插入字典树,枚举每个距离 \(dis[i]\),维护当前字典树上的节点 \(node\),使得 \(node\) 能往与当前位不同的节点走就往不同的节点走即可

代码

C++
#include<bits/stdc++.h>
using namespace std;
#define ll long long
const int MAXN = 1e5+5, MAXL = 4e6+5;

int n, cnt;
ll ans;
ll dis[MAXN];

struct Edge{
    int to;
    ll weig;
};
vector <Edge> G[MAXN];

struct Node{
    int nex[2];
    bool exist;
}trie[MAXL];

void insert(ll x){
    int node = 0;
    for (int i=31; i>=0; i--){
        int c = ((x>>i)&1);
        if (!trie[node].nex[c]) cnt++, trie[node].nex[c]=cnt;
        node = trie[node].nex[c];
    }
    trie[node].exist = true;
}

void getans(ll x){
    int node = 0;
    ll res = 0;
    for (int i=31; i>=0; i--){
        int c = ((x>>i)&1);
        if (trie[node].nex[(c^1)]) node=trie[node].nex[(c^1)], res = (res|(1<<i));
        else node = trie[node].nex[c];
    }
    ans = max(ans, res);
}

void dfs(int node, int fa, ll rdis){
    dis[node] = rdis;
    for (int i=0; i<G[node].size(); i++){
        if (G[node][i].to == fa) continue;
        dfs(G[node][i].to, node, (rdis^G[node][i].weig));
    }   
}

void show(int node, string s){
    if (trie[node].exist) cout << s << endl;
    if (trie[node].nex[0]) show(trie[node].nex[0], s+"0");
    if (trie[node].nex[1]) show(trie[node].nex[1], s+"1");
}

int main(){
    scanf("%d", &n);
    for (int i=1; i<=n-1; i++){
        int node1, node2;
        ll weig;
        scanf("%d%d%lld", &node1, &node2, &weig);
        G[node1].push_back({node2, weig});
        G[node2].push_back({node1, weig});
    }

    dfs(1, 1, 0);

    for (int i=1; i<=n; i++){
        insert(dis[i]);
    }
    // show(0, "");
    for (int i=1; i<=n; i++){
        getans(dis[i]);
    }

    printf("%lld", ans);
    return 0;
}

P3294

P3294

先总结题意,设当前位置为 \(x\),总共有 \(n\) 个单词 1. 如果当前单词存在一个后缀,其位置在 \(x\) 后,花费加 \(n^2\) 2. 如果当前单词没有后缀,花费加 \(x\) 3. 如果当前单词所有后缀的位置都在 \(x\) 前,设这些位置中的最大值为 \(y\),花费加 \(x-y\)

我们发现,第 \(1\) 个情况的花费开销实在太大,远远高过后两种情况,因此我们要避免,即 使得单词的所有后缀都在填在其前面;对于另外两种情况,实质上情况 \(2\) 是情况 \(3\)\(y=0\) 时的特殊情况,因此只需要考虑情况 \(3\) 即可

显然我们需要处理单词间的后缀关系,很自然的想法就是 将每个单词反转插入字典树;字典树中非实际存在的节点没有用,因此我们进一步建出其 重构树,实质上即每个节点的最长后缀向该节点连边形成的树;我们考虑在其上进行操作

其实感性理解上,我们发现将树上的每条链连在一起的这种排法看起来比较优秀,因为再把任何一个其他链上的节点插进去会增加链后所有节点的花费;这实际上是 \(dfs\)

尝试证明这一结论;发现所有的花费中都含 \(x\),因此只需考虑 \(-y\),即我们想让 \(\sum-y\) 最小,即 \(\sum y\) 最大;对于位置 \(x\),其相应的 \(y\) 显然满足 \(y \le x\),我们想让 \(y\) 尽量大,那么最好就与 \(x\) *相邻* (或者就是 \(x\));如何让其相邻?实际上就是把链上的节点连在一起,中间没有其他链的节点;即得证

现在我们可以把每条链看做一个整体;还没完,链的长度显然也会对答案产生影响;对于节点 \(i\) 每个子树的根节点 \(j\),其价值开销就是在 \(j\)\(i\) 所有儿子的子树大小和,那么肯定 从小到大排序 最优;其实很容易理解,这就相当于排队打水计算每个人等待时间的和,肯定让打水时间少的人站在前面更优

实现上,对于重构树,我们使用并查集记录节点 \(x\)\(fa[x]\) 表示在 \(x\) 上方存在且最近的节点 (如果上方没有也可以是 \(x\) 自身),维护并查集,在搜到存在的节点 \(i\) 时使 \(last[i]\)\(i\) 连边即可

别忘了排序,统计答案时直接按照 \(dfs\) 序即可

代码

C++
#include<bits/stdc++.h>
using namespace std;
#define ll long long
const int MAXL = 5e5+5, MAXN = 1e5+5;

int n, cnt, recnt;
int fa[MAXL];
ll ans;
string s;

struct Node{
    int child[26];
    bool exist;
    Node(){memset(child, 0, sizeof(child)); exist=false;}
}trie[MAXL];

struct ReNode{
    vector <int> child;
    int siz;
    ReNode(){child.clear(); siz=0;}
}retrie[MAXL];

void insert(string s){
    int node = 1;
    for (int i=s.size()-1; i>=0; i--){
        int u = s[i]-'a';
        if (!trie[node].child[u]) cnt++, trie[node].child[u] = cnt;
        node = trie[node].child[u];
    }
    if (!trie[node].exist) trie[node].exist = true;
    return;
}

void dfs(int node){
    if (trie[node].exist == true && node>1){
        retrie[fa[node]].child.push_back(node);
        fa[node] = node;
    }
    for (int i=0; i<26; i++){
        if (trie[node].child[i]){
            fa[trie[node].child[i]] = fa[node]; 
            dfs(trie[node].child[i]);   
        }
    }
}

void dfssiz(int node){
    retrie[node].siz = 1;
    for (int i=0; i<retrie[node].child.size(); i++){
        dfssiz(retrie[node].child[i]);
        retrie[node].siz += retrie[retrie[node].child[i]].siz;
    }
}

bool cmp(int a, int b){
    return retrie[a].siz < retrie[b].siz;
}

void dfssort(int node){
    sort(retrie[node].child.begin(), retrie[node].child.end(), cmp);
    for (int i=0; i<retrie[node].child.size(); i++){
        dfssort(retrie[node].child[i]); 
    }    
}

void get(int node){
    int tmp = recnt;
    recnt++;
    for (int i=0; i<retrie[node].child.size(); i++){
        ans += recnt-tmp;
        get(retrie[node].child[i]);
    }   
}

int main(){
    ios::sync_with_stdio(false);
    cin.tie(0), cout.tie(0);

    cnt = 1;
    cin >> n;
    for (int i=1; i<=n; i++){
        cin >> s;
        insert(s);
    }
    trie[1].exist = true;
    fa[1] = 1;
    dfs(1);
    dfssiz(1);
    dfssort(1);
    get(1);

    cout << ans;
    return 0;
}

P2292

P2292

字典树什么东西,直接上自动机

有一个显然的 \(dp\),记 \(dp[i]\) 表示文本串前 \(i\) 位是否能被理解 (可行性 \(dp\)),转移即对于每个 \(i\),枚举 \(j(1 \le j \le i)\),若 \(s[j]s[j+1]...s[i]\) 是字典中的一个单词且 \(dp[j-1]\) 为真,\(dp[i]\) 即为真

初值即为 \(dp[0] = true\)

对于文本串的前缀,我们发现需要处理出其所有在字典中的后缀进行转移,这很显然就是 \(AC\) 自动机,一直跳 \(fail\) 即可;直接做会超时,考虑进行玄学优化

记录字典中单词的最大长度 \(maxl\),对于文本串当前位 \(i\),维护 \(i\) 之前可以被理解的最大位置 \(maxpl\),显然当 \(maxl+maxpl < i\) 时可以直接返回假;另外,当 \(dp[i]\) 已经为真时就没必要继续跳了,直接返回即可;这样就水过了此题

代码

C++
#include<bits/stdc++.h>
using namespace std;
const int MAXL = 2e6+5, MAXT = 2e4+5;

int n, m, cnt, maxl, maxpl;
bool dp[MAXL];
string s;

struct Node{
    int child[26];
    int fail;
    int len;
    int exist;

    Node(){memset(child, 0, sizeof(child)); fail = len = exist = 0;}
}trie[MAXT];

void insert(string s){
    int node = 1;
    for (int i=0; i<s.size(); i++){
        int u = s[i]-'a';
        if (!trie[node].child[u]) cnt++, trie[node].child[u] = cnt;
        node = trie[node].child[u];
        trie[node].len = i+1;
    }
    if (!trie[node].exist) trie[node].exist=1;
    maxl = max(maxl, trie[node].len);
    return;
}

queue <int> q;

void build(){
    for (int i=0; i<26; i++) trie[0].child[i] = 1;
    trie[1].fail = 0;
    q.push(1);

    while (!q.empty()){
        int node = q.front();
        q.pop();
        int fail = trie[node].fail;

        for (int i=0; i<26; i++){
            int v = trie[node].child[i];
            if (v){
                trie[v].fail = trie[fail].child[i];
                q.push(v);
            }
            else{
                trie[node].child[i] = trie[fail].child[i];
            }
        }   
    }
}

void query(string s){
    int node = 1;
    for (int i=0; i<s.size(); i++){
        int u = s[i]-'a';
        node = trie[node].child[u];
        for (int j=node; j>1; j=trie[j].fail){
            if (trie[j].exist == 1){
                dp[i+1] = (dp[i+1] | dp[i+1-trie[j].len]);
                if (dp[i+1] == true){maxpl = max(maxpl, i+1); break;}
                if (maxpl+maxl < i+1) break;                
            }
        }
    }       
}

int main(){
    ios::sync_with_stdio(false);
    cin.tie(0), cout.tie(0);

    cnt = 1;
    cin >> n >> m;
    for (int i=1; i<=n; i++){
        cin >> s;
        insert(s);
    }
    build();
    for (int i=1; i<=m; i++){
        memset(dp, false, sizeof(dp));
        dp[0] = true;
        maxpl = 0;
        cin >> s;
        query(s);
        cout << maxpl << endl;
    }
    return 0;
}

P2922

P2922

我们在字典树中插入每个 \(b_i\);设当前处理的文本为 \(c\),维护字典树上每个节点 \(node\) 被包含的次数 \(cnt1\) (注意当单词在该节点结束时也统计) 与在 \(node\)结束的单词数 \(cnt2\)

这样,设 \(c\) 在字典树上的终止节点为 \(node\),对于 \(c\) 包含 \(b_i\) 的情况,我们只需要统计从根到 \(node\) 上的 \(\sum cnt2\);对于 \(b_i\) 包含 \(c\) 的情况,我们只需要统计 \(node\) 上的 \(cnt1\);需要注意,由于我们在单词的终止节点同时增加了 \(cnt1\)\(cnt2\) 的贡献,因此这样我们会 多算一遍 \(node\) 上的 \(cnt2\)

最终答案即为 \(\sum cnt2+cnt1_{node}-cnt2_{node}\)

代码

C++
#include<bits/stdc++.h>
using namespace std;
const int MAXL = 5e5+5;

int N, M, num, nc, cnt, res;
string tmp;

struct Node{
    int nex[2];
    bool exist;
    int cnt1, cnt2;
    Node(){nex[0]=nex[1]=0;exist=false;cnt1=cnt2=0;}
}trie[MAXL];

void insert(string s){
    int node = 0;
    for (int i=0; i<s.size(); i++){
        int c = s[i]-'0';
        if (!trie[node].nex[c]) cnt++,trie[node].nex[c]=cnt;
        node = trie[node].nex[c];
        trie[node].cnt1++;
    }
    trie[node].cnt2++;
}

int query(string s){
    int node = 0, res = 0;
    for (int i=0; i<s.size(); i++){
        int c = s[i]-'0';
        if (!trie[node].nex[c]) return res;
        node = trie[node].nex[c];
        res += trie[node].cnt2;
    }
    res += (trie[node].cnt1-trie[node].cnt2);
    return res;
}

int main(){
    scanf("%d%d", &N, &M);
    for (int i=1; i<=N; i++){
        scanf("%d", &num);
        tmp = "";
        for (int j=1; j<=num; j++){
            scanf("%d", &nc);
            tmp += (nc+'0');
        }
        insert(tmp);
    }
    for (int i=1; i<=M; i++){
        scanf("%d", &num);
        tmp = "";
        for (int j=1; j<=num; j++){
            scanf("%d", &nc);
            tmp += (nc+'0');
        }
        printf("%d\n", query(tmp));
    }
    return 0;
}

树形DP基础 做题记录

P2015

P2015

典型的树上背包模型。我们设 \(dp[i][j]\) 表示在以 \(i\) 为根的子树中保留 \(j\) 条边的最大价值;按照树形 \(DP\) 的一般套路,在节点 \(node\) 处,首先递归搜索儿子的子树,再进行转移;

转移时,设 \(j\) 为当前以 \(node\) 为根的子树选择的边数,\(k\) 为以 \(child\) 为根的子树选择的边数,连接节点 \((i, j)\) 的边的权值为 \(val[i][j]\),转移方程即为 \(dp[node][j] = \sum_{child \in son(node)}max(dp[node][j-k-1]+dp[child][k]+val[node][child])\),注意这里因为 还有 \((node, child)\) 的一条边,所以转移方程中第二维 \(j-k-1\)\(k\) 的和是 \(j-1\) 而非 \(j\)

实现时,我们可以考虑记录节点 \(i\) 子树中边的个数 \(siz[i]\),方便判断边界;\(siz[i]\) 的转移方程为 \(siz[node] = \sum_{child \in son(node)}siz[child]+1\),同样别忘了考虑 \((node, child)\) 的一条边

答案即为 \(dp[1][Q]\)

代码

C++
#include<bits/stdc++.h>
using namespace std;
const int MAXN = 105, MAXQ = 105;

struct Edge{
    int to;
    int weig;
};

int N, Q;
vector <Edge> G[MAXN];
int dp[MAXN][MAXQ], siz[MAXN];

void dfs(int node, int fa){
    for (int i=0; i<G[node].size(); i++){
        if (G[node][i].to == fa) continue;
        dfs(G[node][i].to, node);
        siz[node] += siz[G[node][i].to]+1;
        for (int j=min(siz[node], Q); j>=1; j--){
            for (int k=0; k<=min(siz[G[node][i].to], j-1); k++){
                dp[node][j] = max(dp[node][j], dp[node][j-k-1]+dp[G[node][i].to][k]+G[node][i].weig);
            }
        }
    }
}

int main(){
    scanf("%d%d", &N, &Q);

    for (int i=1; i<=N-1; i++){
        int node1, node2, weig;
        scanf("%d%d%d", &node1, &node2, &weig);
        G[node1].push_back({node2, weig});
        G[node2].push_back({node1, weig});
    }

    dfs(1, 1);
    printf("%d", dp[1][Q]);
    return 0;
}

P2014

P2014

仍然是树上背包问题。转移方程与 \(P2015\) 是类似的;实现时,有几个细节略有不同;我们将每门课的先修课向该课连边,最终会形成一个森林,如果我们依次 \(dp\) 每棵树并统计不太方便,因此考虑 增加一个 \(0\) 号点,作为所有没有先修课的课程的先修课,设其价值为 \(0\),再进行树形 \(DP\) 即可;别忘了我们在这里增加了一个点,对枚举的上界与最终的答案选择会产生影响

C++
#include<bits/stdc++.h>
using namespace std;
const int MAXN = 105;

int N, M;
vector <int> G[MAXN];
int dp[MAXN][MAXN], siz[MAXN], in[MAXN], w[MAXN];

void dfs(int node, int fa){
    for (int i=0; i<G[node].size(); i++){
        if (G[node][i] == fa) continue;
        dfs(G[node][i], node);
        siz[node] += siz[G[node][i]]+1;
        for (int j=min(M+1, siz[node]); j>=1; j--){
            for (int k=1; k<=min(siz[G[node][i]], j-1); k++){
                dp[node][j] = max(dp[node][j], dp[node][j-k]+dp[G[node][i]][k]);
            }
        }
    }
}

int main(){
    scanf("%d%d", &N, &M);

    w[0] = 0;
    for (int i=1; i<=N; i++){
        int node, weig;
        scanf("%d%d", &node, &weig);
        G[node].push_back(i);
        in[i]++;
        w[i] = weig;
    }

    for (int i=1; i<=N; i++){
        if (in[i] == 1) siz[i]++;
        dp[i][1] = w[i];
    }

    dfs(0, 0);
    printf("%d", dp[0][M+1]);
    return 0;
}

BNDSOJ DP-5-3 祖孙询问

DP-5-3 祖孙询问

可以使用 \(LCA\) 卡过,如果 \(x\)\(y\) 的祖先那么 \(lca(x, y) = x\),如果 \(y\)\(x\) 的祖先那么 \(lca(x, y) = y\),如果都不是那么 \(lca(x, y)\) 既不是 \(x\) 也不是 \(y\)

但是,这道题中只需要判断 \(x\)\(y\) 是否在同一子树中,没有必要使用 \(LCA\);我们考虑与 \(Tarjan\) 类似的思想,在对树进行 \(dfs\) 时,我们开两个数组 \(in[i], out[i]\),分别表示节点 \(i\) 什么时候第一次被遍历,\(i\) 什么时候退出遍历;

对于节点 \(i\)\(j\),如果 \(i\)\(j\) 的祖先,那么 一定有 \(in[i] < in[j]\ \&\&\ out[i] \ge out[j]\),证明是显然的;如果两节点不在一个子树中,那么两节点的 \(in, out\) 区间不交

代码

C++
#include<bits/stdc++.h>
using namespace std;
const int MAXN = 1e5+5;

int n, m, s, diff=0;
int in[MAXN], out[MAXN];
vector <int> G[MAXN];

void dfs(int node, int fa){
    diff++;
    in[node] = diff;
    for (int i=0; i<G[node].size(); i++){
        if (G[node][i] == fa) continue;
        dfs(G[node][i], node);
    }
    out[node] = diff;
}

int main(){
    scanf("%d", &n);

    for (int i=1; i<=n; i++){
        int node1, node2;
        scanf("%d%d", &node1, &node2);
        if (node2 == -1){
            s = node1; 
            continue;
        }
        G[node1].push_back(node2);
        G[node2].push_back(node1);
    }

    dfs(s, s);

    scanf("%d", &m);

    for (int i=1; i<=m; i++){
        int node1, node2;
        scanf("%d%d", &node1, &node2);
        if (in[node1] < in[node2] && out[node1] >= out[node2]) printf("1\n");
        else if (in[node1] > in[node2] && out[node1] <= out[node2]) printf("2\n");
        else printf("0\n");
    }
    return 0;
}

BNDSOJ DP-5-5 容易题

DP-5-5 容易题

由于我们从一个点开始 \(dfs\),达到的最远点必定是直径的一个端点,因此先求出直径 \(\delta(s, t)\),再分别以 \(s\)\(t\) 为根进行两次 \(dfs\) 求出其他点到这两点的距离即可;对于点 \(node\),答案即为 \(max\{\delta(node, s), \delta(node, t)\}\)

代码

C++
#include<bits/stdc++.h>
using namespace std;
const int MAXN = 10005;

int N, c, node1, node2;
int d[MAXN], d1[MAXN], d2[MAXN];
vector <int> G[MAXN];

void dfs(int node, int fa){
    for (int i=0; i<G[node].size(); i++){
        if (G[node][i] == fa) continue;
        d[G[node][i]] = d[node]+1;
        if (d[G[node][i]] > d[c]) c = G[node][i];
        dfs(G[node][i], node);
    }   
}

void get1(int node, int fa){
    for (int i=0; i<G[node].size(); i++){
        if (G[node][i] == fa) continue;
        d1[G[node][i]] = d1[node]+1;
        get1(G[node][i], node);
    }
}
void get2(int node, int fa){
    for (int i=0; i<G[node].size(); i++){
        if (G[node][i] == fa) continue;
        d2[G[node][i]] = d2[node]+1;
        get2(G[node][i], node);
    }
}

int main(){
    scanf("%d", &N);

    for (int i=1; i<=N-1; i++){
        int node1, node2;
        scanf("%d%d", &node1, &node2);
        G[node1].push_back(node2);
        G[node2].push_back(node1);
    }

    dfs(1, 1);
    d[c] = 0;
    node1 = c;
    dfs(c, c);
    node2 = c;

    get1(node1, node1);
    get2(node2, node2);

    for (int i=1; i<=N; i++){
        printf("%d\n", max(d1[i], d2[i]));
    }
    return 0;
}

P2016

P2016

题意即为求给定树的最小点覆盖;我们注意到,一条边的两个端点至少要选一个,因此设 \(dp[i][0/1]\) 表示在点 \(i\) 处设置 / 不设置士兵所花的最小代价;

转移方程即为 \(dp[i][1] = \sum_{v \in son(i)}min(dp[v][0], dp[v][1])\)\(dp[i][0] = \sum_{v \in son(i)}dp[v][1]\),在 \(dfs\) 转移前别忘了设初值 \(dp[i][1] = 1\)

代码

C++
#include<bits/stdc++.h>
using namespace std;
const int MAXN = 2e3+5, INF = 2e9;

int n, id, k, node;
vector <int> G[MAXN];
int dp[MAXN][2];

void dfs(int node, int fa){
    dp[node][1] = 1;
    for (int i=0; i<G[node].size(); i++){
        if (G[node][i] == fa) continue;
        dfs(G[node][i], node);
        dp[node][1] += min(dp[G[node][i]][0], dp[G[node][i]][1]);
        dp[node][0] += dp[G[node][i]][1];
    }
}

int main(){
    scanf("%d", &n);

    for (int i=1; i<=n; i++){
        scanf("%d%d", &id, &k);
        for (int i=1; i<=k; i++){
            scanf("%d", &node);
            G[id].push_back(node);
            G[node].push_back(id);
        }
    }

    dfs(0, 0);
    printf("%d", min(dp[0][0], dp[0][1]));
    return 0;
}

P2458

P2458

与上一道题略有不同,上一道题希望覆盖所有的边,这道题希望覆盖所有的点;由于一个点被覆盖可能通过三种方式,我们考虑设 \(dp[i][0/1/2]\),表示在该点处设置保安 / 在该点的 父亲 处设置保安 / 在该点的 儿子 处设置保安的最小代价

我们一个一个来看,对于 \(dp[i][0]\),由于当前节点已经被覆盖,儿子节点 可以选择任意方式,因此转移方程为 \(dp[i][0] = \sum_{v \in son(i)}min(dp[v][0], dp[v][1], dp[v][2])\)

对于 \(dp[i][1]\),设此时 \(i\) 的孩子为 \(v\),由于 \(i\) 现在并没有设置保安,因此 不能通过 \(dp[v][1]\) 转移而来;因此把这种情况挖掉,转移方程即为 \(dp[i][1] = \sum_{v \in son(i)}min(dp[i][0], dp[i][2])\)

对于 \(dp[i][2]\),同理此时不能通过 \(dp[v][1]\) 转移而来,因此转移方程即为 \(dp[i][2] = \sum_{v \in son(i)}min(dp[i][0], dp[i][2])\) ……了吗?事情没有这么简单;我们发现,如果节点 \(i\)所有儿子 \(v\) 都选择通过 \(v\) 的儿子来覆盖 (即通过 \(dp[v][2]\) 转移而来),那么实际上所有的儿子 \(v\) 上都没有保安,这与状态设计不符;这种情况下,我们只能考虑选取一个儿子 \(v\),将由 \(dp[v][2]\) 转移换成由 \(dp[v][0]\) 转移,因此我们也要在转移时 记录每个儿子 \(dp[v][0]-dp[v][2]\) 的最小值 \(extra\),最终特判如果全都选择由 \(dp[v][2]\) 转移而来,则将结果加上 \(extra\)

实现时,别忘了将叶子节点的 \(dp[i][2]\) 设为 \(+\infty\),同时将每个 \(dp[i][0]\) 赋初值 \(v[i]\)

代码

C++
#include<bits/stdc++.h>
using namespace std;
const int MAXN = 2005, INF = 1e9;

int n, start, id, k, m, child, cover;
int r[MAXN];
bool nroot[MAXN], leaf[MAXN];

vector<int>G[MAXN];
long long dp[MAXN][3];

void dfs(int node){
    dp[node][0] = r[node];
    bool flag = true;
    long long cover = 1ll*INF;

    for (int i=0; i<G[node].size(); i++){
        dfs(G[node][i]);

        dp[node][0] += min(dp[G[node][i]][0], min(dp[G[node][i]][1], dp[G[node][i]][2]));
        dp[node][1] += min(dp[G[node][i]][0], dp[G[node][i]][2]);

        if (dp[G[node][i]][0] < dp[G[node][i]][2]){
            flag = false;
            dp[node][2] += dp[G[node][i]][0];
        }
        else{
            cover = min(1ll*cover, dp[G[node][i]][0]-dp[G[node][i]][2]);
            dp[node][2] += dp[G[node][i]][2];
        }
    }

    if (flag == true){
        dp[node][2] += cover;
    }
}

int main(){
    scanf("%d", &n);

    for (int i=1; i<=n; i++){
        scanf("%d%d%d", &id, &k, &m);
        r[id] = k;
        if (m == 0) leaf[id] = true;
        for (int i=1; i<=m; i++){
            scanf("%d", &child);
            nroot[child] = true;
            G[id].push_back(child);
        }
    }

    for (int i=1; i<=n; i++){
        if (leaf[i] == true){
            dp[i][2] = INF; 
        }
        if (nroot[i] == false){
            start = i;
        }
    }

    dfs(start);
    printf("%lld", min(dp[start][0], dp[start][2]));
    return 0;
}

BNDSOJ DP-5-8 旅游规划

DP-5-8 旅游规划

题意为让我们求出所有在直径上的节点 (直径可能有很多条);显然只按常规操作进行二次 \(dfs\) 无法解决问题 (因为只能求出一条直径上的节点),但又因为暴力选取每个点分别进行 \(dfs\) 时间复杂度为 \(O(n^2)\),会超时,我们希望优化算法

首先,注意到任选树上一节点 \(s\) 进行 \(dfs\),到达的节点肯定是直径的端点,这很显然,反证即可;我们在 \(dfs\) 中维护以 \(s\) 为根时所有节点的深度 \(d[i]\) 与最终直径的长度 \(maxd\),搜索完成后枚举每个节点 \(i\),记录下所有 \(d[i] = maxd\) 的节点 \(i\);由于我们可以任意选取直径,易知所有的节点 \(i\) 都为某一条直径的端点,同时,所有的直径都至少有一个端点在这些节点中

接下来,如果对于每个端点都再进行一次 \(dfs\),仍然会超时;我们考虑只选取一个端点 \(node\) 再进行 \(dfs\),按照同样的方法记录下所有离 \(node\) 最远的节点 \(k\);同理,所有的节点 \(k\) 也都为某一条直径的端点;又因为 所有直径中点都相交,则 所有的节点 \(k\) 和所有的节点 \(i\) 已经覆盖了所有的直径的端点

现在我们已经得到了所有的直径的端点,但题目让我们求所有直径上的点,怎么办呢?我们 在第二次 \(dfs\) 考虑记录每个节点的父亲,依次往上跳即可;那为什么可以覆盖所有直径上的点?我们设一条直径为 \(\delta(s, t)\),另一条为 \(\delta(p, q)\),现在我们将节点 \(s\) 作为根跑 \(dfs\),由上,易知 \(\delta(s, t)\)\(\delta(p, q)\) 有交点,设为 \(mid\),在 \(\delta(p, s)\) 上的所有节点,都能够在 \(p \rightarrow mid \rightarrow s\) 这么跳时被覆盖,同理在 \(\delta(q, s)\) 上的所有节点都能够在 \(q \rightarrow mid \rightarrow s\) 这么跳时被覆盖;因此证毕

代码

C++
#include<bits/stdc++.h>
using namespace std;
const int MAXN = 2e5+5;

int n, maxd, nod;
int d[MAXN], jump[MAXN];
vector <int> G[MAXN];
set <int> res, ans;

void dfs(int node, int fa){
    for (int i=0; i<G[node].size(); i++){
        if (G[node][i] == fa) continue;
        d[G[node][i]] = d[node]+1;
        if (d[G[node][i]] > maxd) maxd = d[G[node][i]];
        dfs(G[node][i], node);
    }
}
void dfs2(int node, int fa){
    jump[node] = fa;
    for (int i=0; i<G[node].size(); i++){
        if (G[node][i] == fa) continue;
        d[G[node][i]] = d[node]+1;
        if (d[G[node][i]] > maxd) maxd = d[G[node][i]];
        dfs2(G[node][i], node);
    }
}

int main(){
    scanf("%d", &n);

    for (int i=1; i<=n-1; i++){
        int node1, node2;
        scanf("%d%d", &node1, &node2);
        G[node1].push_back(node2);
        G[node2].push_back(node1);
    }

    dfs(0, -1);
    for (int i=1; i<=n; i++){
        if (d[i] == maxd){
            nod = i;
            res.insert(i);
        }
    }
    maxd = 0, d[nod] = 0;
    dfs2(nod, -1);
    for (int i=1; i<=n; i++){
        if (d[i] == maxd){
            res.insert(i);
        }
    }
    for (auto i=res.begin(); i!=res.end(); i++){
        int recent = (*i);
        while (recent != -1){
            ans.insert(recent);
            recent = jump[recent];
        }
    }
    for (auto i=ans.begin(); i!=ans.end(); i++){
        printf("%d\n", (*i));
    }
    return 0;
}

区间DP基础 做题记录

BNDSOJ DP-3-2 能量项链

BNDSOJ DP-3-2 能量项链

发现给定珠子序列为长度为 \(n\) 的一个环,那么显然需要破环成链;具体的,将整个序列复制一份接到原序列末尾,区间 DP 已经保证了统计的区间长度不会超过 \(n\),统计答案时枚举起点取最大值即可

不妨依据题意设 \(dp[i][j]\) 表示将区间 \([i, j]\) 间的珠子合并所能得到的最大能量;设第 \(i\) 颗珠子上的值为 \(v[i]\),考虑转移,则枚举 \(k(i<k<j)\) 作为 "分割点",\(dp[i][j] = max(dp[i][k]+dp[k][j]+v[i] \times v[k] \times v[j])\),即先分别统计将 \([i, k]\)\([k, j]\) 合并能获得的价值的最大值,最终加上本次合并获得的价值即 \(v[i] \times v[k] \times v[j]\)

时间复杂度 \(O(n^3)\)

C++
#include<bits/stdc++.h>
using namespace std;
const int MAXN = 205;

int n, ans=0;
int num[MAXN];
int dp[MAXN][MAXN];

int main(){
    scanf("%d", &n);

    for (int i=1; i<=n; i++){
        scanf("%d", &num[i]);
        num[n+i] = num[i];
    }

    for (int l=1; l<=n; l++){
        for (int i=1; i<=2*n-l; i++){
            int lhs=i, rhs=i+l;
            for (int k=lhs+1; k<rhs; k++){
                dp[lhs][rhs] = max(dp[lhs][rhs], dp[lhs][k]+dp[k][rhs]+num[lhs]*num[k]*num[rhs]);
            }
        }
    }

    for (int i=1; i<=n; i++){
        ans = max(ans, dp[i][i+n]);
    }
    printf("%d", ans);
    return 0;
}

BNDSOJ DP-3-3 乘法游戏

BNDSOJ DP-3-3 乘法游戏

发现题目中要求的并不是合并牌而是拿走牌,因此满足区间 DP 的性质,考虑设 \(dp[i][j]\) 表示将区间 \([i, j]\) 内的数合并能得到的最大价值,设第 \(i\) 个数为 \(v[i]\);考虑转移,枚举转移点 \(k(i<k<j)\),则 \(dp[i][j] = max(dp[i][k]+dp[k][j]+v[i] \times v[k] \times v[j])\)

初始状态为 \(dp[i][i]=0(1 \le i \le n)\)\(dp[i][i+1]=0(1 \le i < n)\),因为无法合并长度小于 \(3\) 的区间;题目中要求不能选取第 \(1\) 个与第 \(n\) 个数,但区间 DP 的转移点一定满足在 \((1, n)\) 范围内,不会越出,因此不需要特殊考虑

答案即为 \(dp[1][n]\)

时间复杂度 \(O(n^3)\)

C++
#include<bits/stdc++.h>
using namespace std;
const int MAXN = 205;

int n;
int num[MAXN];
int dp[MAXN][MAXN];

int main(){
    scanf("%d", &n);
    memset(dp, 0x3f, sizeof(dp));

    for (int i=1; i<=n; i++){
        scanf("%d", &num[i]);
        dp[i][i] = 0;
    }
    for (int i=1; i<n; i++){
        dp[i][i+1] = 0;
    }

    for (int l=1; l<=n; l++){
        for (int i=1; i<=n-l+1; i++){
            int lhs=i, rhs=i+l-1;
            for (int k=lhs+1; k<rhs; k++){
                dp[lhs][rhs] = min(dp[lhs][rhs], dp[lhs][k]+dp[k][rhs]+num[lhs]*num[k]*num[rhs]);
            }
        }
    }

    printf("%d", dp[1][n]);
    return 0;
}

BNDSOJ DP-3-4 石子合并

BNDSOJ DP-3-4 石子合并

由于最大值与最小值本质相同,以下只考虑最大值

首先注意到石子排成了一个环,因此还是考虑破环成链,将原序列复制一份接在原序列的后面;设第 \(i\) 堆石子价值为 \(v[i]\),合并区间 \([i, j]\) 的石子的价值为 \(val(i, j)\),考虑设 \(dp[i][j]\) 表示将区间 \([i, j]\) 中的石子合并能获取的最大价值,仍然枚举 \(k(i \le k<j)\) 作为转移点,\(dp[i][j] = max(dp[i][k]+dp[k+1][j]+val(i, j))\);由于一次合并获得的是两堆石子的价值之和,合并一次区间就获得一次区间内所有石子的价值之和,因此 \(val(i, j) = \sum_{k=i}^{j} v[k]\),可以使用前缀和维护

对于初始状态,由于一次合并最少合并相邻的两堆石子,因此 \(dp[i][i]=0 (1 \le i \le n)\)

时间复杂度 \(O(n^3)\)

C++
#include<bits/stdc++.h>
using namespace std;
const int MAXN = 405;

int n, ans=0, ans2=2147483647;
int num[MAXN], sum[MAXN];
int dp[MAXN][MAXN], dp2[MAXN][MAXN];

int main(){
    scanf("%d", &n);
    memset(dp2, 0x3f, sizeof(dp2));

    for (int i=1; i<=n; i++){
        scanf("%d", &num[i]);
        num[n+i] = num[i];
    }
    for (int i=1; i<=2*n; i++){
        sum[i] = sum[i-1]+num[i];
        dp2[i][i] = 0;
    }

    for (int l=2; l<=n; l++){
        for (int i=1; i<=2*n-l-1; i++){
            int lhs=i, rhs=i+l-1;
            for (int k=lhs; k<rhs; k++){
                dp[lhs][rhs] = max(dp[lhs][rhs], dp[lhs][k]+dp[k+1][rhs]+sum[rhs]-sum[lhs-1]);
                dp2[lhs][rhs] = min(dp2[lhs][rhs], dp2[lhs][k]+dp2[k+1][rhs]+sum[rhs]-sum[lhs-1]);  
            }
        }   
    }

    for (int i=1; i<=n-1; i++){
        ans = max(ans, dp[i][i+n-1]);
        ans2 = min(ans2, dp2[i][i+n-1]);
    }
    printf("%d\n%d", ans2, ans);
    return 0;
}

BNDSOJ DP-3-5 括号配对

BNDSOJ DP-3-5 括号配对

令第 \(i\) 个字符为 \(s[i]\),设 \(dp[i][j]\) 表示将区间 \([i, j]\) 变为 \(GBE\) 的最少操作数,仍然考虑枚举 \(k(i \le k<j)\),由于区间中的同一位置只需要修改一次,因此 \(dp[i][j]=min(dp[i][k]+dp[k+1][j])\);注意第二个区间的左端点为 \(k+1\),枚举 \(k\) 的范围包含 \(i\)

但如果这么转移,实际上忽略了 "如果 \(A\)\(GBE\),那么 \([A]\)\((A)\) 也是 \(GBE\)" 的条件,因此可以很容易举出反例,比如输入 [(],会输出 3,实际上应该输出 1;那么如何加上这一条件?当我们合并区间 \([i, j]\) 时,如果 \(s[i]\)\(s[j]\) 构成了 [] 或者 (),就只需要考虑合并 \([i+1, j-1]\),因此在转移后判断 \(s[i]\)\(s[j]\) 是否构成 [](),如果构成就加上 $dp[i][j] = min(dp[i][j], dp[i+1][j-1]) $ 即可

初始条件为 \(dp[i][i] = 1 (1 \le i \le n)\),答案即为 \(dp[i][n]\)

时间复杂度 \(O(n^3)\)

C++
#include<bits/stdc++.h>
using namespace std;
const int MAXN = 505, MAXINT = 2e9;

string s;
int dp[MAXN][MAXN];

int main(){
    cin >> s;

    for (int i=0; i<s.size(); i++){
        dp[i+1][i+1] = 1;
    }

    for (int l=2; l<=s.size(); l++){
        for (int i=1; i<=s.size()-l+1; i++){
            int lhs=i, rhs=i+l-1;
            dp[lhs][rhs] = MAXINT;
            for (int k=lhs; k<rhs; k++){
                dp[lhs][rhs] = min(dp[lhs][rhs], dp[lhs][k]+dp[k+1][rhs]);
            }

            if ((s[lhs-1] == '(' && s[rhs-1] == ')') || (s[lhs-1] == '[' && s[rhs-1] == ']')){
                dp[lhs][rhs] = min(dp[lhs][rhs], dp[lhs+1][rhs-1]);
            }
        }   
    }

    printf("%d", dp[1][s.size()]);
    return 0;
}

BNDSOJ DP-4-2 凸多边形划分

BNDSOJ DP-4-2 凸多边形划分

我们考虑设 \(dp[i][j]\) 表示将以 \(i\) 为开头,\(j\) 为结尾的多边形分割后可以获得的最大价值;注意到每个边都肯定会用且仅用一次,因此我们枚举分割点 \(k (i<k<j)\),使得 \(i, j, k\) 构成一个三角形,再套路地统计 \(dp[i][k]\)\(dp[k][j]\) 即可;转移方程即为 \(dp[i][j] = min(dp[i][j], dp[i][k]+dp[k][j]+num[i] \times num[k] \times num[j])\)

值域较大,\(long long\) 存不下,记得开 \(int128\)

时间复杂度 \(O(n^3)\)

C++
#include<bits/stdc++.h>
using namespace std;
const int MAXN = 55;
const __int128 INF = 1e35;

__int128 read128(){
    __int128 x=0;bool f=1;char ch=getchar();
    while (ch<'0'||ch>'9'){if(ch=='-') f=!f;ch=getchar();}
    while (ch>='0'&&ch<='9'){x=(x<<1)+(x<<3)+(ch^48);ch=getchar();}
    x=(f?x:-x);return x;
}
inline void put128(__int128 x){
    if (x<0) putchar('-'), x=-x;
    if (x>9) put128(x/10);
    putchar(x%10+'0'); return;
}

int N;
__int128 num[MAXN];
__int128 dp[MAXN][MAXN];

int main(){
    scanf("%d", &N);

    for (int i=1; i<=N; i++){
        num[i]=read128();
    }

    for (int len=3; len<=N; len++){
        for (int i=1; i<=N-len+1; i++){
            int lhs=i, rhs=i+len-1;
            dp[lhs][rhs] = INF;
            for (int k=lhs+1; k<rhs; k++){
                dp[lhs][rhs] = min(dp[lhs][rhs], dp[lhs][k]+dp[k][rhs]+num[lhs]*num[k]*num[rhs]);
            }
        }
    }

    put128(dp[1][N]);
    return 0;
}

DP基础 做题记录

BNDSOJ DP-1-1 最长不下降子序列

BNDSOJ DP-1-1 最长不下降子序列

对于序列 \(a_i\),设 \(f[i]\) 表示以 \(i\) 结尾的最长不下降子序列长度,循环枚举 \(j\) \((1 \leq j \leq i-1)\),若 \(a_j \leq a_i\),则可以从 \(f[j]\) 转移至 \(f[i]\),具体来说 \(f[i] = f[j]+1\)

时间复杂度 \(O(n^2)\)

C++
#include<bits/stdc++.h>
using namespace std;
const int MAXN = 10005;

int n, ans=0;
int num[MAXN], f[MAXN];

int main(){
    scanf("%d", &n);

    for (int i=1; i<=n; i++){
        scanf("%d", &num[i]);
    }
    for (int i=1; i<=n; i++){
        f[i] = 1;
    }

    for (int i=1; i<=n; i++){
        for (int j=1; j<i; j++){
            if (num[j] <= num[i]){
                f[i] = max(f[i], f[j]+1);
            }
        }
    }

    for (int i=1; i<=n; i++){
        ans = max(ans, f[i]);
    }

    printf("%d", ans);
    return 0;
}

BNDSOJ DP-1-2 友好城市

BNDSOJ DP-1-2 友好城市

使用结构体记录对应的友好城市,首先按照河岸一边城市的横坐标 \(a_i\) 进行排序

设两对友好城市横坐标为 \((a_1, b_1)\)\((a_2, b_2)\),若希望使得两对友好城市不 "相交",当且仅当 (\(a_1 \leq a_2\) 且 $ b_1 \leq b_2\() 或 (\)a_1 \geq a_2$ 且 \(b_1 \geq b_2\))

因此,希望排序后河岸另一边城市的横坐标 \(b_i\) 单调不降,问题即转化为对序列 \(b_i\) 求最长不下降子序列

时间复杂度 \(O(n^2)\)

C++
#include<bits/stdc++.h>
using namespace std;
const int MAXN = 5005;

int N, ans=0;
int f[MAXN];

struct City{
    int x;
    int y;
}pos[MAXN];

bool cmp(City a, City b){
    return a.x < b.x;
}

int main(){
    scanf("%d", &N);

    for (int i=1; i<=N; i++){
        scanf("%d%d", &pos[i].x, &pos[i].y);
    }

    sort(pos+1, pos+N+1, cmp);

    for (int i=1; i<=N; i++){
        f[i] = 1;
    }

    for (int i=1; i<=N; i++){
        for (int j=1; j<i; j++){
            if (pos[j].y <= pos[i].y){
                f[i] = max(f[i], f[j]+1);
            }
        }
    }

    for (int i=1; i<=N; i++){
        ans = max(ans, f[i]);
    }

    printf("%d", ans);
    return 0;
}

BNDSOJ DP-1-4 贝茜的晨练计划

BNDSOJ DP-1-4 贝茜的晨练计划

首先考虑使用 \(dp[i][j]\) 表示第 \(i\) 分钟疲劳度为 \(j\) 能跑的最远距离,但是注意到贝茜一旦开始休息就必须休息到疲劳度为 \(0\),状态不好转移

由于休息到疲劳度为 \(0\) 的条件很强,考虑特化状态,令 \(dp[i]\) 表示第 \(i\) 分钟且疲劳度为 \(0\) 时能跑的最远距离,显然答案为 \(dp[N]\)

考虑转移,首先可以由上一分钟疲劳度就为 \(0\) 转移得来,相当于上一分钟选择休息,没有多跑任何距离,此时转移方程为 \(dp[i] = dp[i-1]\)

其次,可以在该分钟前时间差大于 \(1\) 的位置转移过来;具体的,由于复杂度从 \(0\) 变成 \(0\),一定是跑了一段时间又持续休息了一段时间,而这两段时间显然相等;为方便枚举,不妨设当前时间为 \(i\),这段时间为 \(j\ (j<=min(i/2, M))\),由于贝茜在第 \(i-2 \times j+1\) 到第 \(i-j\) 分钟选择了跑步,因此转移方程为 \(dp[i] = dp[i-2 \times j]+ \sum_{k=i-2 \times j+1}^{i-j} D[k]\),显然可以用前缀和维护

时间复杂度 \(O(NM)\)

C++
#include<bits/stdc++.h>
using namespace std;
const int MAXN = 1e4+5;

int N, M;
int f[MAXN], D[MAXN], sum[MAXN];

int main(){
    scanf("%d%d", &N, &M);

    for (int i=1; i<=N; i++){
        scanf("%d", &D[i]);
        sum[i] = sum[i-1]+D[i];
    }

    for (int i=1; i<=N; i++){
        f[i] = max(f[i], f[i-1]);

        for (int j=1; j<=min(i/2, M); j++){
            f[i] = max(f[i], f[i-2*j]+sum[i-j]-sum[i-2*j]);
        }
    }

    printf("%d", f[N]);
    return 0;
}

BNDSOJ DP-1-5 ZBRKA

BNDSOJ DP-1-5 ZBRKA

对于一个长度为 \(n-1\)\(1\)\(n-1\) 的排列,设其逆序对数为 \(C\),注意到我们想要往排列中插入数 \(n\) 形成新的排列,又因为 \(n\) 比原排列中的所有数都大,因此可以新形成 \(0\)\(n-1\) 个逆序对,即对长度为 \(n\) 的逆序对数为 \(C\)\(C+n-1\) 的排列的数量贡献都为 \(1\)

具体的,设 \(dp[i][j]\) 表示长度为 \(i\) 的且逆序对数量为 \(j\) 个的排列个数,转移方程为 \(dp[i][j] = \sum_{k=j-(i-1)}^{j} dp[i-1][k]\),显然可以使用前缀和优化,可以压掉一维,但注意每次在转移之前需要先实时更新前缀和数组,同时赋值 \(dp[0] = sum[0] = 1\),因为逆序对数为 \(0\) 时也有一种方案满足 (即 \(1\ 2\ 3\ ...\ n\))

时间复杂度 \(O(NC)\)

C++
#include<bits/stdc++.h>
using namespace std;
#define int long long
const int MAXC = 1e5+5, MOD = 1e9+7;

int N, C;
int f[MAXC], sum[MAXC];

signed main(){
    scanf("%lld%lld", &N, &C);

    for (int i=2; i<=N; i++){
        f[0] = sum[0] = 1;
        for (int j=1; j<=C; j++){
            sum[j] = (sum[j-1]+f[j])%MOD;
        }
        for (int j=0; j<=C; j++){
            if (j >= i){
                f[j] = (sum[j] - sum[j-i])%MOD;
            }
            else{
                f[j] = sum[j]%MOD;
            }
        }
    }

    if (f[C] < 0){
        printf("%lld", MOD+f[C]);
        return 0;   
    }
    printf("%lld", f[C]);
    return 0;
}

BNDSOJ DP-1-8 潜水员

BNDSOJ DP-1-8 潜水员

注意到选择一个气缸会增加两个变量,及氧和氮的数量,因此这是一个二维费用背包,令 \(dp[k][i][j]\) 表示前 \(k\) 个气缸中氧气为数量 \(i\) 且氮气数量为 \(j\) 时总共的最小重量

由于希望气缸总重最小,给 \(dp\) 数组初始赋极大值,同时注意初始化 \(dp[0][0] = 0\),令氧气数量为 \(a[i]\),氮气数量为 \(b[i]\),气缸重量为 \(v[i]\),则转移方程为 \(dp[k][i][j] = min(dp[k-1][i][j], dp[k-1][i-a[k]][j-b[k]]+v[k])\),第一维可以压掉

注意转移时,不一定氧的数量与氮的数量的上界一定是 \(m\)\(n\),因此设定一个偏移量 \(DIF = 80\),氧和氮的上界分别为 \(m+DIF\)\(n+DIF\)

注意枚举答案时要枚举到所装多于 \(m\)\(n\) 时的状态,取最大值

时间复杂度 \(O(kmn)\)

C++
#include<bits/stdc++.h>
using namespace std;
const int MAXK = 1e4+5, MAXQ = 1005, DIF = 80;

int m, n, ans=2147483647;
int k;
int a[MAXK], b[MAXK], c[MAXK];
int dp[MAXQ][MAXQ];

int main(){
    memset(dp, 0x7f, sizeof(dp));
    dp[0][0] = 0;
    scanf("%d%d", &m, &n);
    scanf("%d", &k);

    for (int i=1; i<=k; i++){
        scanf("%d%d%d", &a[i], &b[i], &c[i]);
    }

    for (int l=1; l<=k; l++){
        for (int i=m+DIF; i>=a[l]; i--){
            for (int j=n+DIF; j>=b[l]; j--){
                dp[i][j] = min(dp[i][j], dp[i-a[l]][j-b[l]]+c[l]);
            }
        }
    }

    for (int i=m; i<=m+DIF; i++){
        for (int j=n; j<=n+DIF; j++){
            ans = min(ans, dp[i][j]);
        }
    }

    printf("%d", ans);
    return 0;
}

BNDSOJ DP-1-10 金明的预算方案

BNDSOJ DP-1-10 金明的预算方案

对于每个主件及其的配件,只有 买主件、买主件和 \(1\) 个附件、买主件和 \(2\) 个附件三种购买方案,且只能选择一种,因此可以视为分组背包

注意价值为 \(v[i] \times w[i]\) 而非 \(v[i]\)

时间复杂度 \(O(NM)\)

C++
#include<bits/stdc++.h>
using namespace std;
const int MAXT = 505, MAXN = 1e5+5;

int N, M, T=0;
int cnt[MAXT], belong[MAXT];
int w[MAXT][MAXT], v[MAXT][MAXT];
int f[MAXN];

int main(){
    scanf("%d%d", &N, &M);

    for (int i=1; i<=M; i++){
        int price, value, type;
        scanf("%d%d%d", &price, &value, &type);
        if (type == 0){
            T++;
            cnt[T]++;
            belong[i] = T;
            w[T][cnt[T]] = price;
            v[T][cnt[T]] = price*value;
        }
        else{
            cnt[belong[type]]++;
            w[belong[type]][cnt[belong[type]]] = w[belong[type]][cnt[belong[type]]-1]+price;
            v[belong[type]][cnt[belong[type]]] = v[belong[type]][cnt[belong[type]]-1]+price*value;
        }   
    }

    /*
    for (int i=1; i<=T; i++){
        cout << "object " << i << endl;
        cout << "main weight: " << w[i][1] << " value: " << v[i][1] << endl;
        cout << "side1 weight: " << w[i][2] << " value: " << v[i][2] << endl;
        cout << "side2 weight: " << w[i][3] << " value: " << v[i][3] << endl;
    }
    */

    for (int k=1; k<=T; k++){
        for (int i=N; i>=0; i--){
            for (int j=1; j<=cnt[k]; j++){
                if (i >= w[k][j]){
                    f[i] = max(f[i], f[i-w[k][j]]+v[k][j]);
                }
            }
        }
    }

    printf("%d", f[N]);
    return 0;
}

BNDSOJ DP-2-2 橱窗布置

BNDSOJ DP-2-2 橱窗布置

定义状态 \(dp[i][j]\) 表示前 \(i\) 朵花中,第 \(i\) 朵花放在第 \(j\) 个花瓶时美学值的最大值

考虑转移,注意到我们已经固定了 \(i\) 的位置,因此不难想到进一步固定 \(i-1\) 的位置,进行转移;由于第 \(i-1\) 朵花至少放在第 \(i-1\) 个花瓶中,至多放在第 \(j-1\) 个花瓶中,因此转移方程为 \(dp[i][j] = \sum_{k=i-1}^{j-1} max(dp[i-1][k]+beauty[i][j])\) (\(beauty[i][j]\) 表示第 \(i\) 朵花在第 \(j\) 个花瓶中的美学值)

由于还需要记录方案,考虑用 \(from[i][j]\) 保存一个 \(pair<int, int>\),表示其由哪个状态转移过来,递归输出即可

时间复杂度 \(O(FV^2)\)

C++
#include<bits/stdc++.h>
using namespace std;
const int MAXF = 505, MAXV = 505;

int F, V, ans=-2147483647, final=-1;
int beauty[MAXF][MAXV];
int dp[MAXF][MAXV];
pair <int, int> from[MAXF][MAXV];

void getans(int step, int pos){
    if (step == 1){
        printf("%d ", pos);
        return;
    }
    // cout << "debug: " << pos << " " << from[pos].second << endl;
    getans(from[step][pos].first, from[step][pos].second); 
    printf("%d ", pos);
}

int main(){
    scanf("%d%d", &F, &V);
    memset(dp, -0x3f, sizeof(dp));
    dp[0][0] = 0;

    for (int i=1; i<=F; i++){
        for (int j=1; j<=V; j++){
            scanf("%d", &beauty[i][j]);
        }
    }

    for (int i=1; i<=F; i++){
        for (int j=i; j<=V-(F-i); j++){
            for (int k=i-1; k<=j-1; k++){
                if (dp[i-1][k]+beauty[i][j] > dp[i][j]){
                    dp[i][j] = dp[i-1][k]+beauty[i][j]; 
                    from[i][j] = {i-1, k};
                }
            }
        }
    }

    for (int i=F; i<=V; i++){
        if (dp[F][i] > ans){
            ans = max(ans, dp[F][i]);
            final = i;
        }
    }
    printf("%d\n", ans);
    getans(F, final);
}

BNDSOJ DP-2-3 花匠

BNDSOJ DP-2-3 花匠

题目要求的实际上是一个交替增减的子序列,由于 \(O(n^2)\) 的时间复杂度会超时,因此考虑只从序列中的上一位转移状态,因此发现实际上只需要考虑序列变化的趋势,即如果趋势变化则贡献 \(+1\),若不变化则贡献也不变

因此设 \(dp[i][0/1]\) 表示在原序列第 \(i\) 位变化趋势为 \(0/1\) (\(0\) 表示当前变化趋势为上升,\(1\) 表示当前变化趋势为下降) 的留下的花的最大值;设 \(a_i\) 为原序列的第 \(i\) 项,分情况讨论,当 \(a_i > a_{i-1}\) 时,\(dp[i][0] = max(dp[i-1][0], dp[i-1][1]+1)\)\(dp[i][1] = dp[i-1][1]\),当 \(a_i < a_{i-1}\) 时,\(dp[i][1] = max(dp[i-1][1], dp[i-1][0]+1)\)\(dp[i][0] = dp[i-1][0]\),当 \(a_i = a_{i-1}\) 时,\(dp[i][0] = dp[i-1][0]\)\(dp[i][1] = dp[i-1][1]\)

边界条件显然为 \(dp[1][0] = dp[1][1] = 1\),输出时直接输出 \(max(dp[n][0], dp[n][1])\) 即可,\(n\) 为原序列长度,本质上实际是递推

时间复杂度 \(O(n)\)

C++
#include<bits/stdc++.h>
using namespace std;
const int MAXN = 1e5+5;

int n, ans=0;
int num[MAXN];
int dp[MAXN][2];

int main(){
    scanf("%d", &n);

    for(int i=1; i<=n; i++){
        scanf("%d", &num[i]);
    }

    dp[1][0] = dp[1][1] = 1;

    for (int i=2; i<=n; i++){
        if (num[i] > num[i-1]){
            dp[i][0] = max(dp[i-1][0], dp[i-1][1]+1);
            dp[i][1] = dp[i-1][1];
        }
        else if (num[i] < num[i-1]){
            dp[i][1] = max(dp[i-1][1], dp[i-1][0]+1);
            dp[i][0] = dp[i-1][0];
        }
        else{
            dp[i][0] = dp[i-1][0];
            dp[i][1] = dp[i-1][1];
        }
    }

    printf("%d", max(dp[n][0], dp[n][1]));
    return 0;
}

BNDSOJ DP-2-4 编辑距离

\(dp[i][j]\) 为将 \(A\) 串前 \(i\) 位变为 \(B\) 串前 \(j\) 位所需的最小代价,考虑由 \(i-1\)\(j-1\) 转移而来,由于支持修改、添加和删除,实际上有如下几种方式

  1. \(A\) 串前 \(i-1\) 个变为 \(B\) 串前 \(j-1\) 个,再将 \(A_i\) 修改为 \(B_j\) 即可,分两种情况,如果 \(A_i = B_j\)\(dp[i][j] = dp[i-1][j-1]+1\),否则 \(dp[i][j] = dp[i-1][j-1]+1\)
  2. \(A\) 串前 \(i-1\) 个变为 \(B\) 串前 \(j\) 个,再将 \(A_i\) 删除即可,\(dp[i][j] = dp[i-1][j]+1\)
  3. \(A\) 串前 \(i\) 个变为 \(B\) 串前 \(j-1\) 个,再在 \(A\) 中插入 \(B_j\) 即可,\(dp[i][j] = dp[i][j-1]+1\)

\(A\) 的长度为 \(n\)\(B\) 的长度为 \(m\),则初始情况为 \(dp[0][i] = i(1<=i<=m)\),即将空串变为 \(B\) 的前 \(i\) 个字符,需要插入 \(i\) 次;\(dp[i][0] = i(1<=i<=n)\),即将 \(A\) 的前 \(i\) 个字符变为空串,需要删除 \(i\) 次;最终答案即为 \(dp[n][m]\)

时间复杂度 \(O(nm)\)\(n\)\(m\) 为串长

C++
#include<bits/stdc++.h>
using namespace std;
const int MAXN = 5005;
string a, b;

int dp[MAXN][MAXN];

int main(){
    cin >> a >> b;

    for (int i=0; i<a.size(); i++){
        dp[i+1][0] = i+1;
    }
    for (int i=0; i<b.size(); i++){
        dp[0][i+1] = i+1;
    }

    for (int i=0; i<a.size(); i++){
        for (int j=0; j<b.size(); j++){
            if (a[i] == b[j]){
                dp[i+1][j+1] = min(dp[i][j], min(dp[i][j+1]+1, dp[i+1][j]+1));
            }
            else{
                dp[i+1][j+1] = min(dp[i][j]+1, min(dp[i][j+1]+1, dp[i+1][j]+1));    
            }
        }
    }

    cout << dp[a.size()][b.size()];
    return 0;
}

BNDSOJ DP-2-5 乌龟棋

BNDSOJ DP-2-5 乌龟棋

首先考虑设 \(dp[i][a][b][c][d]\) 表示走到第 \(i\) 个格子用了 \(a\)\(1\) 卡片,\(b\)\(2\) 卡片,\(c\)\(3\) 卡片,\(d\)\(4\) 卡片,但是这样 \(dp\) 会超时;我们注意到一个很关键的点,只需要各用了几张不同类型的卡片,实际上就已经可以算出当前到的格子位置,因此第一维实际上是没有用的

\(dp[a][b][c][d]\) 为 (走到当前格子) 四种卡分别用了多少张时能获得价值的最大值,显然当前格子位置为 \(chart = a \times 1 + b \times 2 + c \times 3 + d \times 4\),因为可以通过在距离当前格子前 \(1/2/3/4\) 个位置的格子使用 一个 \(a/b/c/d\) 类型的卡片从而前进到当前格子,因此转移方程即为 \(dp[a][b][c][d] = max(dp[a-1][b][c][d], dp[a][b-1][c][d], dp[a][b][c-1][d], dp[a][b][c][d-1])+val[chart]\),其中 \(val[i]\) 为在第 \(i\) 个格子可以获得的价值;注意 \(a/b/c/d = 0\) 时不能再由 \(a-1/b-1/c-1/d-1\) 转移,需要特殊考虑

初始情况为 \(dp[0][0][0][0] = val[1]\),因为题目中说了自动获得起点位置的价值

时间复杂度 \(O(A \times B \times C \times D)\)\(A/B/C/D\) 为四种卡片的个数

C++
#include<bits/stdc++.h>
using namespace std;
const int MAXN = 405;

int N, M;
int num[MAXN], cnt[5];
int dp[45][45][45][45];

int main(){ 
    scanf("%d%d", &N, &M);

    for (int i=1; i<=N; i++){
        scanf("%d", &num[i]);   
    }
    for (int i=1; i<=M; i++){
        int val;
        scanf("%d", &val);
        cnt[val]++;
    }

    dp[0][0][0][0] = num[1];

    for (int i=0; i<=cnt[1]; i++){
        for (int j=0; j<=cnt[2]; j++){
            for (int k=0; k<=cnt[3]; k++){
                for (int l=0; l<=cnt[4]; l++){
                    int chart = i*1+j*2+k*3+l*4+1;
                    if (i!=0){
                        dp[i][j][k][l] = max(dp[i][j][k][l], dp[i-1][j][k][l]+num[chart]);
                    }
                    if (j!=0){
                        dp[i][j][k][l] = max(dp[i][j][k][l], dp[i][j-1][k][l]+num[chart]);
                    }
                    if (k!=0){
                        dp[i][j][k][l] = max(dp[i][j][k][l], dp[i][j][k-1][l]+num[chart]);
                    }
                    if (l!=0){
                        dp[i][j][k][l] = max(dp[i][j][k][l], dp[i][j][k][l-1]+num[chart]);
                    }
                }
            }
        }
    }

    printf("%d", dp[cnt[1]][cnt[2]][cnt[3]][cnt[4]]);
    return 0;
}

BNDSOJ DP-2-6 传纸条

BNDSOJ DP-2-6 传纸条

由于有两路传纸条路径不能相交的限制,直接跑两次最大值 \(dp\) 不能保证正确性;

考虑设 \(dp[i_1][j_1][i_2][j_2]\) 为第一路纸条传到 \((i_1, j_1)\) 且第二路纸条传到 \((i_2, j_2)\) 时的最大值;关键在于使得两条路线不会相交,观察到两条路线必然一条在 "右上",一条在 "左下",钦定 \((i_1, j_1)\) 所在路线在 "右上",因此枚举 \(i_1(1<=i_1<=n)\)\(j_1(1<=j_1<=m)\)\(i_2(i_1<i_2<=n)\)\(j_2(1<=j_2<j_1)\),这样保证 \((i_2, j_2)\) 所在线路与 \((i_1, j_1)\) 线路总不相交,转移时直接由上下左右移动一个位置转移即可,具体的,\(dp[i_1][j_1][i_2][j_2] = max(dp[i_1-1][j_1][i_2-1][j_2], dp[i_1][j_1-1][i_2-1][j_2], dp[i_1-1][j_1][i_2][j_2-1], dp[i_1][j_1-1][i_2][j_2-1])+val[i_1][j_1]+val[i_2][j_2]\),其中 \(val[i][j]\) 表示在 \((i, j)\) 位置的同学的好心程度

不需要另设边界条件,因为没有开始传时最大价值就是 \(0\);注意最终答案为 \(dp[n-1][m][n][m-1]\),因为显然 "右上" 线的终点为 \((n-1, m)\) 即右下角上方,"左上" 线的终点为 \((n, m-1)\) 即右下角左方

时间复杂度 \(O(N^2M^2)\)

代码中的 \(n\)\(m\) 和思路中是反过来的

C++
#include<bits/stdc++.h>
using namespace std;
const int MAXM = 55, MAXN = 55;

int m, n;
int mp[MAXM][MAXN];
int f[MAXM][MAXN][MAXM][MAXN];

int main(){
    scanf("%d%d", &m, &n);

    for (int i=1; i<=m; i++){
        for (int j=1; j<=n; j++){
            scanf("%d", &mp[i][j]);
        }
    }

    for (int i=1; i<=m; i++){
        for (int j=1; j<=n; j++){
            for (int k=i+1; k<=m; k++){
                for (int l=1; l<j; l++){
                    f[i][j][k][l] = max(
                        max(f[i][j-1][k][l-1], f[i][j-1][k-1][l]), 
                        max(f[i-1][j][k][l-1], f[i-1][j][k-1][l])
                    );
                    f[i][j][k][l] += (mp[i][j]+mp[k][l]);
                }
            }
        }
    }

    printf("%d", f[m-1][n][m][n-1]);
    return 0;
}