山东大学计算机科学与技术学院程序设计思维与实践作业 week14-动态规划(4)

山东大学计算机科学与技术学院程序设计思维与实践作业
山大程序设计思维与实践作业
sdu程序设计思维与实践
山东大学程序设计思维实践作业H14
山大程序设计思维实践作业H14
山东大学程序设计思维与实践
week14-动态规划(4)
相关资料:GitHub

A : 直径数量问题

题目描述
给出一棵树,求树的直径及其数量(最长简单路径的长度和数量)。

输入格式
第一行一个整数 n , 0≤n≤10^5,
接下来有 n−1 行,每行 a,b 代表 a 到第 b 有一条长度为 1 的边。
1≤a,b≤n

输出格式
输出两个数,分别表示树的直径和数量。

样例输入
5
5 1
1 2
2 3
2 4
样例输出
3 2

#include <bits/stdc++.h>
using namespace std;

const int maximumnum = 1e5 + 11;
int diam[maximumnum], far[maximumnum], countd[maximumnum], countf[maximumnum];
vector<int> edge[maximumnum];

void dfs(int x, int f){
    countf[x] = 1; // 最远路径数为1
    // 遍历x的所有除了父节点的邻接点,求最远路的个数
    for(auto &i : edge[x]){
        if(i == f) continue; // 父节点不在子树中
        dfs(i,x); // 对该子树求结果
        // 如果从这个子节点走到的更远,直接更新为子树的这条最远路的个数
        if(far[i] + 1 > far[x]){
            far[x] = far[i] + 1;
            countf[x] = countf[i];
        }
        // 若又找到一条和当前最远路一样长的字节点
        // 就将最远路的长度加上这个子节点带来的最远路径数
        else if(far[i] + 1 == far[x])
            countf[x] += countf[i];
    }

    // 情况1: 由根节点到叶子的一条路径
    diam[x] = far[x];
    countd[x] = countf[x];

    // 情况2: 经过根节点的一条路径

    int Max = 0,subMax = 0;
    int cntn = 0; // 直径经过根节点的方案个数
    if(!((f == -1 && edge[x].size() < 2) || (f != -1 && edge[x].size() < 3))){

        // 求最远距离和第二远的距离
        for(auto &i : edge[x]){
            if(i == f) continue;
            // 若这条子树的最远距离+1比当前第二远的路径还远,更新
            if(far[i] + 1 > subMax) {
                subMax = far[i] + 1;
                // 保证最远的路径比第二远的路径要远
                if(subMax > Max)
                    swap(subMax,Max);
            }
        }

        // 处理
        // 若最远路径和第二远路径大小相同
        if(subMax == Max){
            int cnt = 0;// 记录最大值的数量
            for(auto &i :edge[x]){
                if(i == f) continue;
                // 因为要随机选择两条最远路,需要将其中一条路是该节点的最远路的情况
                // 即用当前的最大值数 * 当前子节点的最远路情况数
                if(far[i] + 1 == subMax){
                    cntn += cnt * countf[i];
                    cnt += countf[i];
                }
            }
        }
        // 若最远路径和第二远路径大小不同
        else{
            int cntMax = 0, cntSubMax = 0;
            for(auto &i :edge[x]){
                if(i == f) continue;
                // 更新最远路Max的情况数
                if(far[i] + 1 == Max)
                    cntMax += countf[i];
                // 更新第二最远路subMax的情况数
                else if(far[i] + 1 == subMax)
                    cntSubMax += countf[i];
            }
            // 结果为两种情况的乘积
            cntn = cntMax * cntSubMax;
        }
    }

    // 情况3: 不经过根节点的一条路径
    for(auto &i :edge[x]){
        // 找到所有以子节点为根的树的直径的最大值即可
        if(i == f) continue;
        // 若该子节点为根的直径大于当前的最大值,就将结果更新为当前子树的直径
        if(diam[i] > diam[x]){
            diam[x] = diam[i];
            countd[x] = countd[i];
        }
        // 若该子节点为根的直径等于当前的最大值,就将结果加上为当前子树的直径情况数
        else if(diam[i] == diam[x])
            countd[x] += countd[i];
    }
    // 比较选择出三种情况中最大的情况
    if(Max + subMax > diam[x]) {
        diam[x] = Max + subMax;
        countd[x] = cntn;
    }
    // 若不同情况中有相同大小的直径,则把各种情况求和
    else if(Max + subMax == diam[x])
        countd[x] += cntn;

}
int main(){
    int n;
    cin >> n;
    int u ,v;
    // 插入边
    while(--n){
        cin >> u >> v;
        edge[u].push_back(v);
        edge[v].push_back(u);
    }
    // dfs求直径
    dfs(1,-1);
    // 输出直径和数量
    cout << diam[1] << " " << countd[1] << endl;
    return 0;
}

B : 城市规划

题目描述
有一座城市,城市中有 N 个公交站,公交站之间通过 N−1 条道路连接,每条道路有相应的长度。保证所有公交站两两之间能够通过唯一的通路互相达到。
两个公交站之间路径长度定义为两个公交站之间路径上所有边的边权和。
现在要对城市进行规划,将其中 M 个公交站定为“重要的”。
现在想从中选出 K 个节点,使得这 K 个公交站两两之间路径长度总和最小。输出路径长度总和即可(节点编号从 1 开始)。

输入格式
第 1 行包含三个正整数 N,M 和 K 分别表示树的节点数,重要的节点数,需要选出的节点数。
第 2 行包含 M 个正整数,表示 M 个重要的节点的节点编号。
接下来 N−1 行,每行包含三个正整数 a,b,c,表示编号为 a 的节点与编号为 b 的节点之间有一条权值为 c 的无向边。每行中相邻两个数之间用一个空格分隔。

输出格式
输出只有一行,包含一个整数表示路径长度总和的最小值。

样例输入
5 3 2
1 3 5
1 2 4
1 3 5
1 4 3
4 5 1
样例输出
4
样例解释
p15.png

样例中的树如上图所示。
重要的节点标号为 1, 3, 5,从中选出两个点,有三种方案:
方案 1: 1, 3 之间路径长度为 5
方案 2: 1, 5 之间路径长度为 4
方案 3: 3, 5 之间路径长度为 9

#include<bits/stdc++.h>
#include<ext/pb_ds/assoc_container.hpp>
#include<ext/pb_ds/tree_policy.hpp>
using namespace std;
using namespace __gnu_pbds;
typedef long long ll;
typedef long double ld;
const int N = 5e4+7;
tree<ll,null_type,less<ll>,rb_tree_tag,tree_order_statistics_node_update> tr;
ll gcd(ll a, ll b){ return a == 0 ? b : gcd(b % a, a); }
ll exgcd(ll a,ll b,ll&x,ll&y){if(b==0){x=1,y=0;return a;}else{ll r=exgcd(b,a%b,x,y),t=x;x=y;y=t-a/b*x;return r;}}//注意x,y一定为ll

int a[N],head[N],cnt=1,vis[N];
ll dp[N][107], inf = 1e15;
struct edge{
    int f,t,n;
    ll w;
}g[N*2];
int n,m,k,x;
void add(int f,int t,ll w){
    g[cnt].n = head[f];
    g[cnt].t = t;
    g[cnt].w = w;
    head[f] = cnt++;
}
void dfs(int u,int fa){
    dp[u][0] = 0;
    if(vis[u]) dp[u][1] = 0;
    for(int i=head[u];i;i=g[i].n){
        int to = g[i].t;
        if(to==fa) continue;
        dfs(to,u);
        for(int j=k;j>=1;j--){
            for(int w=j;w>=0;w--){
                dp[u][j] = min(dp[u][j],dp[u][j-w]+dp[to][w]+g[i].w*w*(k-w));
            }
        }
    }
}
int main(){
    std::ios::sync_with_stdio(false);
    cin.tie(0);
    cin>>n>>m>>k;
    for(int i=1;i<=n;i++)
        for(int j=0;j<=k;j++)
            dp[i][j] = inf;
    for(int i=1;i<=m;i++) cin>>x, vis[x]=1;
    for(int i=0;i<n-1;i++){
        int f,t,w;
        cin>>f>>t>>w;
        add(f,t,w), add(t,f,w);
    }
    dfs(1,0);
    cout<<dp[1][k];
}

##C : 最大区间和

题目描述
输入一个长度为 n 的整数序列 a,从中找出一段不超过 m 的连续子序列(区间),使得这个序列的和最大。选出的区间可以为空。
n≤106,m≤n,−109≤ai≤109

输入描述
第一行两个数 n,m,第二行 n 个整数 ai 表示这个数列。

输出描述
一个整数表示答案。

样例输入
6 3
1 -3 5 1 -2 3
样例输出
6

#include <bits/stdc++.h>
using namespace std;
const int maximumn = 1e6+10;
long long a[maximumn];       /*a数组存区间*/
long long sum[maximumn];    /*sum记录前缀和*/
long long result;
deque<long long> q;//双端队列存下标
int main(){
    int n,m;
    cin>>n>>m;
    for(int i = 1; i <= n; i++) {
        cin>>a[i];
        sum[i] = sum[i-1] + a[i];
    }
    q.push_back(0);
    /*使用一个单调队列维护最小值*/
    for(int i = 1; i <= n; i++){
        /*保证是最小的前缀和*/
        while(!q.empty() && sum[q.back()] > sum[i])
            q.pop_back();
        q.push_back(i);
        /*如果超过了m的范围,进行去除*/
        while(!q.empty() && i - m > q.front())
            q.pop_front();
        
        result = max(result,sum[i] - sum[q.front()]);
    }
    cout<<result<<endl;
    return 0;
}

D : 帝国大厦

题目描述
帝国大厦共有 n 层,LZH 初始时在第 a 层上。
帝国大厦有一个秘密实验室,在第 b 层,这个实验室非常特别,对 LZH 具有约束作用,即若 LZH 当前处于 x 层,当他下一步想到达 y 层时,必须满足 ∣x−y∣<∣x−b∣,而且由于实验室是不对外开放的,电梯无法停留在第 b 层。
LZH 想做一次旅行,即他想按 k 次电梯,他想知道不同的旅行方案个数有多少个。
两个旅行方案不同当前仅当存在某一次按下电梯后停留的楼层不同。

输入格式
一行 4 个数 n,a,b,k。

输出格式
一个数表示答案,由于答案较大,将答案对 109+7 取模后输出。

数据范围与子任务
对于 40% 的数据 n,k≤500。
对于 80% 的数据 n,k≤2000。
对于 100% 的数据 n,k≤5000,1≤a,b≤n,a=b。

测试样例
样例 1
输入:
5 2 4 1
输出:
2
样例 2
输入:
5 2 4 2
输出:
2
样例 3
输入:
5 3 4 1
输出:
0

#include <bits/stdc++.h>
using namespace std;
const long long mod = 1e9 + 7;
const int maximumnum = 5005;
int n,a,b,k;
long long res;
//状态定义
long long dp[maximumnum]; //dp[i]表示到按k次后到第i层的方案书
long long sum[maximumnum]; //sum[i]维护dp[i]前缀和

//使用前缀和,使得不TLE

int main(){
    cin>>n>>a>>b>>k;
    dp[a]  = 1;
    for(int i = a; i <= n; i++)
        sum[i] = 1;
    for(int i = 1; i <= k; i++){  //按下k次电梯
        for(int j = 1; j <= n; j++){  //遍历每一层
            //如果j>b那么dp[j]为从(b+y)/2到n的和(该区间是所有能到达该层的区间)并且去掉本身
            if(j > b) {
                dp[j] = sum[n] - sum[(j+b)>>1] - sum[j] + sum[j-1];
                if(dp[j]> mod)
                    dp[j] =  dp[j]%mod;
                else if(dp[j]< 0)
                    dp[j] =  mod + dp[j];
            }
            //如果j<b,dp【j】为从0到(b+y)/2的和,并且去掉本身
            if(j < b){
                dp[j] = sum[(j+b-1) >> 1] - sum[j] + sum[j-1];
                if(dp[j]> mod)
                    dp[j] =  dp[j]%mod;
                else if(dp[j]< 0)
                    dp[j] =  mod + dp[j];
            }
        }
        for (int j = 1; j <= n; j++)
            sum[j] = (sum[j-1] + dp[j]) % mod;
    }

    sum[n] = sum[n] % mod;
    cout << sum[n] << endl;
    return 0;
}
  • 1
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值