Cocoicobird
热爱永远可以成为你继续下去的理由

今日刷题:\(1033-103\)

1033 NC16416 [NOIP2017]逛公园

题目描述

策策同学特别喜欢逛公园。 公园可以看成一张 \(N\) 个点 \(M\) 条边构成的有向图,且没有自环和重边。其中 \(1\) 号点是公园的入口,\(N\) 号点是公园的出口,每条边有一个非负权值,代表策策经过这条边所要花的时间。
策策每天都会去逛公园,他总是从 \(1\) 号点进去,从 \(N\) 号点出来。
策策喜欢新鲜的事物,他不希望有两天逛公园的路线完全一样,同时策策还是一个特别热爱学习的好孩子,他不希望每天在逛公园这件事上花费太多的时间。如果 \(1\) 号点到 \(N\) 号点的最短路长为 \(d\),那么策策只会喜欢长度不超过 \(d + K\) 的路线。
策策同学想知道总共有多少条满足条件的路线,你能帮帮他吗?
为避免输出过大,答案对 \(P\) 取模。
如果有无穷多条合法的路线,请输出 \(−1\)

输入描述

第一行包含一个整数 \(T\), 代表数据组数。
接下来 \(T\) 组数据,对于每组数据:
第一行包含四个整数 \(N,M,K,P\),每两个整数之间用一个空格隔开。
接下来 \(M\) 行,每行三个整数 \(a_i,b_i,c_i\), 代表编号为 \(a_i,b_i\) 的点之间有一条权值为 \(c_i\) 的有向边,每两个整数之间用一个空格隔开。

输出描述

输出文件包含 \(T\) 行,每行一个整数代表答案。

示例

输入

2
5 7 2 10
1 2 1
2 4 0
4 5 2
2 3 2
3 4 1
3 5 2
1 5 3
2 2 0 10
1 2 0
2 1 0

输出

3
-1

说明
对于第一组数据,最短路为 3。
1 - 5, 1 - 2 - 4 - 5, 1 - 2 - 3 - 5 为 3 条合法路径。

备注

对于不同测试点,我们约定各种参数的规模不会超过如下
image

本题在 noip 数据外额外增加一组数据卡掉了一种错解

解题思路

参考
首先在原图跑一遍 \(Dijkstra\) 求出最短路。
\(dp_{v,k}\) 表示从 \(1\) 走到 \(v\) 并且恰好花费 \(dist_v+k\) 的路线,需要求解该数量。设 \(u->v\) 的边权为 \(w(u,v)\),则有 \(dist_v+k=dist_u+k'+w(u,v)\),可得 \(k'\),也就是在转移的时候需要反向遍历图,因此在输入时存一遍反向图。那么何时会有无穷多路线呢?当图中存在零环时,因此需要对于遍历过的状态进行标记。

C++ 代码
#include <bits/stdc++.h>
using namespace std;
const int N = 100010, M = 2000010;
typedef pair<int, int> PII;

int T, n, m, K, P;
vector<PII> e1[N], e2[N]; // e2 是反向图
int dist[N], dp[N][60];
bool st[N], flag, vis[N][60];

void dijkstra() {
    memset(dist, 0x3f, sizeof dist);
    memset(st, 0, sizeof st);
    priority_queue<PII, vector<PII>, greater<PII>> pq;
    dist[1] = 0;
    pq.push({dist[1], 1});
    while (pq.size()) {
        auto t = pq.top();
        pq.pop();
        int u = t.second;
        if (st[u]) continue;
        st[u] = true;
        for (PII edge: e1[u]) {
            int v = edge.first, w = edge.second;
            if (dist[v] > dist[u] + w) {
                dist[v] = dist[u] + w;
                pq.push({dist[v], v});
            }
        }
    }
}

int dfs(int u, int k) { // 从 1 到 u 比最短路多 k 的方案数
    if (vis[u][k]) { // 存在零环
        flag = true;
        return 0;
    }
    if (~dp[u][k]) return dp[u][k];
    vis[u][k] = true;
    dp[u][k] = 0;
    for (auto edge: e2[u]) { // 这里是反向图 因此原图是 v -> u
        int v = edge.first, w = edge.second;
        // 设置松弛值为 k 则 dist[u] + k = dist[v] + w + k';
        // k' = dist[u] + k - dist[v] - w;
        int tmpk = dist[u] + k - dist[v] - w;
        if (tmpk < 0 || tmpk > K) continue;
        dp[u][k] = (dp[u][k] + dfs(v, tmpk)) % P;
        if (flag) {
            vis[u][k] = false;
            return 0;
        }
    }
    if (u == 1 && k == 0) dp[u][k] = 1;
    vis[u][k] = false;
    return dp[u][k];
}

int main() {
    scanf("%d", &T);
    while (T--) {
        for (int i = 1; i < N; i++)
            e1[i].clear(), e2[i].clear();
        scanf("%d%d%d%d", &n, &m, &K, &P);
        for (int i = 0; i < m; i++) {
            int a, b, c;
            scanf("%d%d%d", &a, &b, &c);
            e1[a].push_back({b, c});
            e2[b].push_back({a, c});
        }
        dijkstra();
        flag = false;
        memset(dp, -1, sizeof dp);
        int ans = 0;
        for (int i = 0; i <= K; i++) {
            ans = (ans + dfs(n, i)) % P;
            if (flag) break;
        }
        if (flag) puts("-1");
        else printf("%d\n", ans);
    }
    return 0;
}

1034 NC20684 wpy的请求

题目描述

wpy移情别恋啦,他不喜欢 spfa 了,现在他喜欢使用 dij,但是他又发现了一个新的问题,dij 无法跑有负权边的图,于是 wpy 找到了她的男朋友也就是你来帮忙,为了你晚上的幸福生活,你必须在 \(1\) 秒内帮她解决这个问题,然后蹿到床上。
简单来说,有一张 \(n\) 个点,\(m\) 条边的有向图,请你给每条边确定一个新的边权(不同边之间可以不同),保证对于任意 \(u\)\(v\),在新图上的 \(u\)\(v\) 的最短路上的点和原图上最短路上的点相同且顺序不变。
新的边权要求非负。

输入描述

第一行两个整数 \(n,m\),表示有 \(n\) 个点,\(m\) 条边。
接下来 \(m\) 行,每行三个数,\(u\)\(v\)\(w\),表示有一条从 \(u\)\(v\)

输出描述

输出 \(m\) 行,每行三个整数 \(u,v,w\) 表示有从 \(u\)\(v\) 的边边权改完后是为 \(w\)
ps:按输入顺序输出。

示例

输入

5 10
1 2 -4383
1 3 -9930
2 4 -7331
1 5 -2175
2 3 11962
2 5 16382
4 5 11420
1 4 37978
3 5 13836
3 4 14617

输出

1 2 0
1 3 0
2 4 0
1 5 0
2 3 17509
2 5 14174
4 5 1881
1 4 49692
3 5 6081
3 4 16401
备注

\(n<=10^3,m<=3*10^3,|w|<=10^9\)
数据保证有解,保证没有负环,保证任意两点间最短路唯一,保证图联通
数据有梯度( 但是出题人也不知道能写什么部分分)

解题思路

Floyd-Warshall 算法可以求解出图内任意两点的最短路径,适用于稠密图,但时间复杂度为 \(O(n^3)\);Dijkstra 算法求解单源最短路径的时间复杂度为 \(O(m+nlogn)\),对每个节点都做一次,也可以达成全源最短路径,但是这个方法仅适用于非负权边图。
Johnson 算法被设计来解决此问题,其支持负权边(当然不能有负权环)。综合两个算法的优势,时间复杂度 \(O(nm+n^2logn)\),在稀疏图中显著优于 Floyd-Warshall,结合了 Dijkstra 的高效性与 Bellman-Ford 的灵活性。具体可参考 Johnson 全源负权最短路径算法详解
对于此题,需要新图的边权都不为负值,且任意两点间的最短路不会发生变化。首先考虑相邻两点 \(u->v\),则 \(dist_v\le dist_u+w\),则 \(\Delta=dist_u+w-dist_v\ge 0\),因此新边权可调整为 \(dist_u+w-dist_v\) 不会出现负权边。那么是否会影响最短路呢?假设原图中的一个路径是 \(p=v_1->v_2->...->v_n\),则其原来的路径长度为 \(W=\sum w(v_i,v_{i+1})\),则根据新边权可得,路径长度 \(W'=\sum (dist_{v_i}+w(v_i,v_{i+1})-dist_{v_{i+1}})\),化简为 \(W'=W+dist_{v_1}-dist_{v_n}\),由于 \(dist_{v_1}-dist_{v_n}\) 是常数,原图与新图上任意对应两个点之间的所有路径都只相差常数,那么原图最短路径仍然是新图最短路径。

C++ 代码
#include <bits/stdc++.h>
using namespace std;
typedef long long LL;
const int N = 1010, M = 4010;
typedef pair<pair<int, int>, int> PPI;

int n, m, idx;
int h[N], e[M], ne[M], w[M];
vector<PPI> edges;
LL dist[N];
bool st[N];


void add(int a, int b, int c) {
    e[idx] = b, w[idx] = c, ne[idx] = h[a], h[a] = idx++;
}

void spfa() {
    memset(dist, 0x3f, sizeof dist);
    queue<int> q;
    q.push(0);
    dist[0] = 0;
    while (q.size()) {
        auto t = q.front();
        q.pop();
        st[t] = false;
        for (int i = h[t]; ~i; i = ne[i]) {
            int j = e[i];
            if (dist[j] > dist[t] + w[i]) {
                dist[j] = dist[t] + w[i];
                if (!st[j]) {
                    q.push(j);
                    st[j] = true;
                }
            }
        }
    }
}

int main() {
    memset(h, -1, sizeof h);
    scanf("%d%d", &n, &m);
    for (int i = 0; i < m; i++) {
        int a, b, c;
        scanf("%d%d%d", &a, &b, &c);
        add(a, b, c);
        edges.push_back({{a, b}, c});
    }
    for (int i = 1; i <= n; i++)
        add(0, i, 0);
    spfa();
    for (int i = 0; i < edges.size(); i++) {
        int u = edges[i].first.first, v = edges[i].first.second, len = edges[i].second;
        cout << u << ' ' << v << ' ' << dist[u] - dist[v] + len << endl;
    }
    return 0;
}

1035 NC50381 道路和航线

题目描述

Farmer John 正在一个新的销售区域对他的牛奶销售方案进行调查。他想把牛奶送到 \(T\) 个城镇(\(1 \le T \le 25,000\)),编号为 \(1\)\(T\)。这些城镇之间通过 \(R\) 条道路(\(1 \le R \le 50,000\),编号为 \(1\)\(R\))和 \(P\) 条航线(\(1 \le P \le 50,000\),编号为 \(1\)\(P\))连接。每条道路 \(i\) 或者航线 \(i\) 连接城镇 \(A_i\)\(1 \le A_i \le T\))到 \(B_i\)\(1 \le B_i \le T\)),花费为 \(C_i\)

对于道路 \(0 \le C_i \le 10,000\);然而航线的花费很神奇,花费 \(C_i\) 可能是负数(\(-10,000 \le C_i \le 10,000\))。道路是双向的,可以从 \(A_i\)\(B_i\),也可以从 \(B_i\)\(A_i\),花费都是 \(C_i\)。然而航线与之不同,只可以从 \(A_i\)\(B_i\)

事实上,由于最近恐怖主义太嚣张,为了社会和谐,出台了一些政策保证:如果有一条航线可以从 \(A_i\)\(B_i\),那么保证不可能通过一些道路和航线从 \(B_i\) 回到 \(A_i\)。由于 FJ 的奶牛世界公认十分给力,他需要运送奶牛到每一个城镇。他想找到从发送中心城镇 \(S\)\(1 \le S \le T\))把奶牛送到每个城镇的最便宜的方案,或者知道这是不可能的。

输入描述

\(R+P+1\) 行。
\(1\) 行:四个整数 \(T,R,P\)\(S\),分别表示城镇的数量,道路的数量,航线的数量和中心城镇。
\(2\)\(R+1\) 行:每行三个整数 \(A_i,B_i\)\(C_i\),描述一条道路。
\(R+2\)\(R+P+1\) 行:每行三个整数 \(A_i,B_i\)\(C_i\),描述一条航线。

输出描述

\(T\) 行,第 \(i\) 行输出城市 \(S\) 到城市 \(i\) 的最小花费。如果不能到达,输出 NO PATH

示例

输入

6 3 3 4 
1 2 5 
3 4 5 
5 6 10 
3 5 -100 
4 6 -100 
1 3 -10 

输出

NO PATH 
NO PATH 
5 
0 
-95 
-100 

说明:一共六个城镇。在 \(1\)\(2\)\(3\)\(4\)\(5\)\(6\) 之间有道路,花费分别是 \(5,5,10\)。同时有三条航线:\(3→5\)\(4→6\)\(1→3\),花费分别是 \(-100,-100,-10\)。FJ 的中心城镇在城镇 \(4\)。FJ 的奶牛从 \(4\) 号城镇开始,可以通过道路到达 \(3\) 号城镇。然后他们会通过航线达到 \(5\)\(6\) 号城镇。但是不可能到达 \(1\)\(2\) 号城镇。

解题思路

将无向边连接的连通块看作一个点,那么这些连通块之间是通过有向边连接的,且不会成环(因为题目中提及的政策),那么就变成了有向无环图,可以使用拓扑排序来更新最短路,而每个连通块内部是非负权图,可以使用 Dijkstra 算法更新每个连通块内部的最短路。

C++ 代码
#include <bits/stdc++.h>
using namespace std;
const int N = 25010, M = 50010;
typedef pair<int, int> PII;

int T, R, P, S;
int h[N], e[M * 3], ne[M * 3], w[M * 3], idx;
int dist[N], st[N];
int id[N], bcnt;
vector<int> block[N];
int d[N];
queue<int> q;

void add(int a, int b, int c) {
    e[idx] = b, w[idx] = c, ne[idx] = h[a], h[a] = idx++;
}

void dfs(int u) {
    block[bcnt].push_back(u);
    id[u] = bcnt;
    for (int i = h[u]; ~i; i = ne[i]) {
        int j = e[i];
        if (!id[j])
            dfs(j);
    }
}

void dijkstra(int s) {
    priority_queue<PII, vector<PII>, greater<PII>> pq;
    for (int u: block[s]) {
        pq.push({dist[u], u});
    }
    while (pq.size()) {
        auto t = pq.top();
        pq.pop();
        int u = t.second;
        if (st[u]) continue;
        st[u] = true;
        for (int i = h[u]; ~i; i = ne[i]) {
            int j = e[i];
            if (dist[j] > dist[u] + w[i]) {
                dist[j] = dist[u] + w[i];
                if (id[j] == s)
                    pq.push({dist[j], j});
            }
            if (id[j] != s && --d[id[j]] == 0)
                q.push(id[j]);
        }
    }
}

void topSort() {
    memset(dist, 0x3f, sizeof dist);
    dist[S] = 0;
    for (int i = 1; i <= bcnt; i++) {
        if (!d[i])
            q.push(i);
    }
    while (q.size()) {
        int t = q.front();
        q.pop();
        dijkstra(t);
    }
}

int main() {
    memset(h, -1, sizeof h);
    scanf("%d%d%d%d", &T, &R, &P, &S);
    for (int i = 1; i <= R; i++) {
        int a, b, c;
        scanf("%d%d%d", &a, &b, &c);
        add(a, b, c);
        add(b, a, c);
    }
    for (int i = 1; i <= T; i++) {
        if (!id[i]) {
            ++bcnt;
            dfs(i);
        }
    }
    for (int i = 1; i <= P; i++) {
        int a, b, c;
        scanf("%d%d%d", &a, &b, &c);
        add(a, b, c);
        d[id[b]]++;
    }
    topSort();
    for (int i = 1; i <= T; i++) {
        if (dist[i] > 0x3f3f3f3f / 2) puts("NO PATH");
        else printf("%d\n", dist[i]);
    }
    return 0;
}

1036 NC20568 [SCOI2012]滑雪与时间胶囊

题目描述

a180285 非常喜欢滑雪。他来到一座雪山,这里分布着 \(m\) 条供滑行的轨道和 \(n\) 个轨道之间的交点(同时也是景点),而且每个景点都有一编号 \(i\space (1 \le i \le n)\) 和一高度 \(h_i\)
a180285 能从景点 \(i\) 滑到景点 \(j\) 当且仅当存在一条 \(i\)\(j\) 之间的边,且 \(i\) 的高度不小于 \(j\)。与其他滑雪爱好者不同,a180285 喜欢用最短的滑行路径去访问尽量多的景点。如果仅仅访问一条路径上的景点,他会觉得数量太少。
于是 a180285 拿出了他随身携带的时间胶囊。这是一种很神奇的药物,吃下之后可以立即回到上个经过的景点(不用移动也不被认为是 a180285 滑行的距离)。
请注意,这种神奇的药物是可以连续食用的,即能够回到较长时间之前到过的景点(比如上上个经过的景点和上上上个经过的景点)。 现在,a180285 站在 \(1\) 号景点望着山下的目标,心潮澎湃。他十分想知道在不考虑时间胶囊消耗的情况下,以最短滑行距离滑到尽量多的景点的方案(即满足经过景点数最大的前提下使得滑行总距离最小)。你能帮他求出最短距离和景点数吗?

输入描述

输入的第一行是两个整数 \(n,m\)。接下来一行有 \(n\) 个整数 \(h_i\),分别表示每个景点的高度。
接下来 \(m\) 行,表示各个景点之间轨道分布的情况。每行三个整数 \(u,v,k\),表示编号为 \(u\) 的景点和编号为 \(v\) 的景点之间有一条长度为 \(k\) 的轨道。

输出描述

输出一行,表示 a180285 最多能到达多少个景点,以及此时最短的滑行距离总和。

示例

输入

3 3 
3 2 1 
1 2 1 
2 3 1 
1 3 10 

输出

3 2
备注

对于 \(30\%\) 的数据,$ 1 \le n \le 2000 $;
对于 \(100\%\) 的数据,$ 1 \le n \le 10^5 , 1 \le m \le 10^6 , 1 \le h_i \le 10^9 , 1 \le k_i \le 10^9 $。

解题思路

从点 \(1\) 开始宽搜确定所能到达的所有点,在搜索过程中,将对应的边保存下来,按照边权排序,依次选择边使得所有能到达的点连在一起,这一步类似 Kruskal 算法。

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

const int N = 100010, M = 2000010;
typedef long long LL;

int n, m;
int a[N];
int h[N], e[M], ne[M], w[M], idx;

int p[N];
bool st[N];

struct Edge {
    int a, b, c;
    bool operator<(const Edge& W) const {
        if (::a[b] != ::a[W.b])
            return ::a[b] > ::a[W.b];
        return c < W.c;
    }
} edges[M];

int tot;

void add(int a, int b, int c) {
    e[idx] = b;
    w[idx] = c;
    ne[idx] = h[a];
    h[a] = idx++;
}

int find(int x) {
    if (p[x] != x)
        p[x] = find(p[x]);
    return p[x];
}

void bfs(int root) {
    queue<int> q;
    q.push(root);
    st[root] = true;
    while (q.size()) {
        int t = q.front();
        q.pop();
        for (int i = h[t]; ~i; i = ne[i]) {
            int v = e[i];
            edges[tot++] = {t, v, w[i]};
            if (!st[v]) {
                q.push(v);
                st[v] = true;
            }
        }
    }
}

int main() {
    memset(h, -1, sizeof h);
    scanf("%d%d", &n, &m);
    for (int i = 1; i <= n; i++) {
        scanf("%d", &a[i]);
        p[i] = i;
    }
    for (int i = 0; i < m; i++) {
        int u, v, k;
        scanf("%d%d%d", &u, &v, &k);
        if (a[u] >= a[v])
            add(u, v, k);
        if (a[v] >= a[u])
            add(v, u, k);
    }
    bfs(1);
    sort(edges, edges + tot);
    LL sum = 0;
    int cnt = 0;
    for (int i = 0; i < tot; i++) {
        int u = find(edges[i].a);
        int v = find(edges[i].b);
        if (u != v) {
            p[u] = v;
            sum += edges[i].c;
            cnt++;
        }
    }
    printf("%d %lld\n", cnt + 1, sum);
    return 0;
}

1037 NC19964 [HAOI2006]聪明的猴子

题目描述

在一个热带雨林中生存着一群猴子,它们以树上的果子为生。昨天下了一场大雨,现在雨过天晴,但整个雨林的地表还是被大水淹没着,部分植物的树冠露在水面上。猴子不会游泳,但跳跃能力比较强,它们仍然可以在露出水面的不同树冠上来回穿梭,以找到喜欢吃的果实。

现在,在这个地区露出水面的有 \(N\) 棵树,假设每棵树本身的直径都很小,可以忽略不计。我们在这块区域上建立直角坐标系,则每一棵树的位置由其所对应的坐标表示(任意两棵树的坐标都不相同)。

在这个地区住着的猴子有 \(M\) 个,下雨时,它们都躲到了茂密高大的树冠中,没有被大水冲走。由于各个猴子的年龄不同、身体素质不同,它们跳跃的能力不同。有的猴子跳跃的距离比较远(当然也可以跳到较近的树上),而有些猴子跳跃的距离就比较近。这些猴子非常聪明,它们通过目测就可以准确地判断出自己能否跳到对面的树上。

现已知猴子的数量及每一个猴子的最大跳跃距离,还知道露出水面的每一棵树的坐标,你的任务是统计有多少个猴子可以在这个地区露出水面的所有树冠上觅食。

输入描述

输入包括:
\(1\) 行为一个整数,表示猴子的个数 \(M\) \((2 \le M \le 500)\)
\(2\) 行为 \(M\) 个整数,依次表示猴子的最大跳跃距离(每个整数值在 \(1 \sim 1000\) 之间);
\(3\) 行为一个整数表示树的总棵数 \(N\) \((2 \le N \le 1000)\)
\(4\) 行至第 \(N+3\) 行为 \(N\) 棵树的坐标(横纵坐标均为整数,范围为:\(-1000 \sim 1000\))。
(同一行的整数间用空格分开)

输出描述

输出包括一个整数,表示可以在这个地区的所有树冠上觅食的猴子数。

示例

输入

4
 1 2 3 4
6
0 0
1 0
1 2
-1 -1
-2 0
2 2

输出

3
备注

对于 \(40\%\) 的数据,保证有 \(2 \le N \le 100\)\(1 \le M \le 100\)
对于全部的数据,保证有 \(2 \le N \le 1000\)\(1 \le M \le500\)

解题思路

确定最小生成树,然后求出最小生成树的最大边权,所有跳跃距离不小于该边权的都可以在这些树上觅食。

C++ 代码
#include <bits/stdc++.h>
using namespace std;
const int N = 1010, M = 510;

int n, m, tot;
int jump[M], p[N];

struct Point {
    int x, y;
} trees[N];

struct Edge {
    int u, v, w;
    bool operator< (const Edge &E) const {
        return w < E.w;
    }
} edges[N * N];

int dist(Point a, Point b) {
    return (a.x - b.x) * (a.x - b.x) + (a.y - b.y) * (a.y - b.y); 
}

int find(int x) {
    if (p[x] != x)
        p[x] = find(p[x]);
    return p[x];
}

int main() {
    scanf("%d", &m);
    for (int i = 1; i <= m; i++)
        scanf("%d", &jump[i]);
    scanf("%d", &n);
    for (int i = 1; i <= n; i++) {
        scanf("%d%d", &trees[i].x, &trees[i].y);
        p[i] = i;
    }
    for (int i = 1; i <= n; i++)
        for (int j = 1; j <= n; j++)
            if (i != j)
                edges[tot++] = {i, j, dist(trees[i], trees[j])};
    sort(edges, edges + tot);
    int len = 0;
    for (int i = 0; i < tot; i++) {
        int u = find(edges[i].u);
        int v = find(edges[i].v);
        if (u != v) {
            p[u] = v;
            len = max(len, edges[i].w);
        }
    }
    int cnt = 0;
    for (int i = 1; i <= m; i++)
        if (jump[i] * jump[i] >= len)
            cnt++;
    printf("%d\n", cnt);
    return 0;
}
posted on 2026-05-11 16:50  Cocoicobird  阅读(13)  评论(0)    收藏  举报