图论姿势总结

42 篇文章 1 订阅
36 篇文章 4 订阅

codeforces786B Legacy  线段树优化建图

题目大意:给出n个点,1~n编号,给出m个建边操作,以如下方式建边:(a,b),(c,d)表示,对于任意两个点x,y,如果a<=x<=b,c<=y<=d,那么在xy之间连边。给定源点求单源最短路 
n<=500000,m<=100000

边1表示权值为1的边,边0表示权值为0的边

最暴力的建图需要 n2m 条边……


题解:这道题目如果老老实实的去建图的话,边会多到爆炸。所以我们需要另外想一个建图方法。

由于边的起点或者终点涉及到了区间的情况,我们可以使用线段树建图的套路。

对于整段区间,我们用把它划分成一颗线段树。

对于整个图,我们可以构造两颗线段树A和B。

在A线段树上,我们从底部往上连权值为0的边。对于B线段树来说我们从顶部往下连边权值为0的边。

然后我们把B线段树底部的所有点向A线段树底部的点连权值为0得边。

对于type=1的边,我们从A线段树底部的点向B线段树底部的点做权值为w的边。

对于type=2的边,我们从A线段树底部的点向B线段树对应的区间段连权值为w的一些边。

对于type=3的边,我们从A线段树对应的区间段向B线段树底部对应的点连权值为w的一些边。

然后跑最短路就行了。

花个丑图。。。。


如果我们把区间拆成点来加边,那么复杂度显然会高达O(n^2logn)的。所以显然不能这样来加边,这里给了一堆线段?我们可以想怎么把线段和点连接起来呢?显然线段树可以轻易的办到,由于是有向边所以这里需要两颗线段树,线段树的节点是2*n的,2颗就是4*n,j加上原来的n个点,所以节点数是5n。然后我们加边原来最坏的O(n),现在可以变成logn了,那么我们的复杂度可以 O(nlognlogn)了。


可以用两个辅助点来表示一次建边操作,两个辅助点分别表示(a,b)->(c,d)/(c,d)->(a,b)。对于(a,b)->(c,d)来说,(a,b)内的点向辅助点连边1,辅助点向(c,d)内的点连边0。 
可是这样做仍然需要 nm 条边。

区间加边想到线段树优化建图 
线段树的每个节点代表一个区间,一棵线段树表示不了进出关系,所以建两棵线段树。 
出线段树每个点向父节点连边0,表示如果能从这个区间出发也就可以从父区间出发。 
入线段树每个点向子节点连边0,表示如果能到达这个区间也就可以到达子区间。 
入线段树每个点向出线段树的平行结点连边0,表示如果能到达这个区间也可以从这个区间出发。 
然后每个操作在线段树上连一连就好了..

最后答案即为出线段树的每个叶子的最短路

然一开始要把A、B树的叶子节点记录下来方便后面加边使用

边数最坏情况应该是每一层左右均取两个区间,一直递归下去,有 log2N层,因此边数大约是

#include <iostream>
#include <cstring>
#include <cstdio>
#include <vector>
#include <queue>
#include <algorithm>
using namespace std;
typedef long long ll;
const ll inf = 0x3f3f3f3f3f3f3f3fLL;
const int maxn = 1e5 + 7;
const int maxv = maxn*5;
int n, m, s;
vector< pair<int, ll> > vt[maxv]; //很难计算边数的话, 用vec很好, 一共5n个点, 2个线段树(2*n)+n个点
int id[2][maxn<<2], idx;
ll dis[maxv];
bool vis[maxv];
struct node
{
    int to;
    ll w;
    node(){}
    node(int tt, ll ww) : to(tt), w(ww) {}
    bool operator < (const node &a)  const
    {
        return w > a.w;
    }
};
void addedge(int u, int v, ll w)
{
    vt[u].push_back(make_pair(v, w));
}
void build(int rt, int l, int r, int type)
{
    id[type][rt] = ++idx; //给线段树每个点的编号
    if(l == r)
    {
        if(type == 0) addedge(id[type][rt], l, 0); //通过这n个点,把两颗线段树的叶子节点连通起来
        else addedge(l, id[type][rt], 0);
        return ;
    }
    int mid = (l+r)>>1;
    build(rt<<1, l, mid, type);
    build(rt<<1|1, mid+1, r, type);
    if(type == 0)
    {
        addedge(id[type][rt], id[type][rt<<1], 0); //入树,从上往下连边
        addedge(id[type][rt], id[type][rt<<1|1], 0);
    }
    else
    {
        addedge(id[type][rt<<1], id[type][rt], 0); //出树, 从下往上连边权为0的
        addedge(id[type][rt<<1|1], id[type][rt], 0);
    }
}
void update(int rt, int l, int r, int i, int j, int type, int u, ll cost)
{
    if(i <= l && r <= j)
    {
        if(type == 0)  //如果是 u -> [l,r] 就是出树的叶子(通过1-n个辅助点)连向入树
            addedge(u, id[type][rt], cost);
        else
            addedge(id[type][rt], u, cost); //反之,反之
        return;
    }
    int mid = (l+r)>>1;
    if(i <= mid) update(rt<<1, l, mid, i, j, type, u, cost);
    if(j > mid) update(rt<<1|1, mid+1, r, i, j, type, u, cost);
}
void dij(int s) //边很多用dir+heap优化很快的
{
    for(int i = 1; i <= 5*n; i++) vis[i] = 0, dis[i] = inf;
    dis[s] = 0;
    priority_queue<node> q;
    q.push(node(s, 0));
//    priority_queue<pair<ll, int> > q;
//    q.push({0, s});
    while(!q.empty())
    {
        int u = q.top().to;
//         int u = q.top().second;
        q.pop();
        if(vis[u]) continue;
        vis[u] = 1;
        for(int i = 0; i < vt[u].size(); i++)
        {
            int to = vt[u][i].first;
            int w = vt[u][i].second;
            if(dis[to] > dis[u] + w)
            {
                dis[to] = dis[u] + w;
                q.push(node(to, dis[to]));
//                 q.push(make_pair(-dis[to], to)); //pair优化的话注意这样。。
            }
        }
    }
}
int main()
{
    while(~scanf("%d%d%d", &n, &m, &s))
    {
        for(int i = 0; i <= 5*n; i++) vt[i].clear();
        memset(id, 0, sizeof(id));
        idx = n;
        build(1, 1, n, 0);
        build(1, 1, n, 1);
        while(m--)
        {
            int cmd, u;
            scanf("%d%d", &cmd, &u);
            if(cmd == 1)
            {
                int v;
                ll cost;
                scanf("%d%lld", &v, &cost);
                addedge(u, v, cost); //直接给两颗线段树借助的点连起来就好了,等于给两颗线段树叶子直接连起来
            }
            else
            {
                int l, r;
                ll cost;
                scanf("%d%d%lld", &l, &r, &cost);
                update(1, 1, n, l, r, cmd-2, u, cost);
            }
        }
        dij(s);
        for(int i = 1; i <= n; i++)
        {
            if(dis[i] == inf) dis[i] = -1;
            printf("%lld%c", dis[i], i == n ? '\n' : ' ');
        }
    }
    return 0;
}
/*
10 8 7
1 10 7 366692903
1 4 8 920363557
2 7 5 10 423509459
2 2 5 7 431247033
2 7 3 5 288617239
2 7 3 3 175870925
3 9 3 8 651538651
3 4 2 5 826387883
*/


BZOJ 2763: [JLOI2011]飞行路线  分层图最短路

题意:给一些无向图, 求从s到t的最短路, 其中有k次可以免费(减少一半,等等条件);

思路: 分层图最短路就好了,代码很易懂

原图既然是一层的。

我们把它拆成k+1层。

每一条边既能连本层,也能连到下一层。

然后直接裸上Dijikstra即可。


代码1:最裸的分层图最短路,真的是分层图

#include <iostream>
#include <cstring>
#include <cstdio>
#include <algorithm>
#include <queue>
#include <vector>
using namespace std;
const int INF = 1e9;
const int maxn = 1200100;
int n, m, k, s, t, cnt, edgcnt;
int head[maxn], dis[maxn], book[maxn];
struct node
{
    int to, val, next;
}edge[maxn<<1];
struct Point
{
    int val, to;
    Point(){}
    Point(int tt, int vv) : to(tt), val(vv){}
    bool operator < (const Point &a) const
    {
        return val > a.val;
    }
};
void addEdge(int u, int v, int w)
{
    edge[edgcnt].to = v;
    edge[edgcnt].val = w;
    edge[edgcnt].next = head[u];
    head[u] = edgcnt++;
}
void init()
{
    edgcnt = 0;
    memset(head, -1, sizeof(head));
}
void dij()
{
    for(int i = 0; i < maxn; i++) dis[i] = INF;
    memset(book, 0, sizeof(book));
    priority_queue<Point> q;
    q.push(Point(s, 0));
    dis[s] = 0;
    while(!q.empty())
    {
        int u = q.top().to;
        q.pop();
        if(book[u]) continue;
        book[u] = 1;
        for(int i = head[u]; i != -1; i = edge[i].next)
        {
            int to = edge[i].to;
            int w = edge[i].val;
            if(dis[u] + w < dis[to])
            {
                dis[to] = dis[u] + w;
                q.push(Point(to, dis[to]));
            }
        }
    }
    int ans  = 0x3f3f3f3f;
    for(int i = 0; i <= k; i++)
        ans = min(ans, dis[t+i*n]);
    printf("%d\n", ans);
}
int main()
{
    while(~scanf("%d%d%d", &n, &m, &k))
    {
        scanf("%d%d", &s, &t);
        init();
        s++, t++;
        while(m--)
        {
            int x, y, z;
            scanf("%d%d%d", &x, &y, &z);
            x++, y++;
            for(int i = 0; i <= k; i++)
            {
                addEdge(x+i*n, y+i*n, z);
                addEdge(y+i*n, x+i*n, z);
                if(i != k)
                {
                    addEdge(x+i*n, y+(i+1)*n, 0);
                    addEdge(y+i*n, x+(i+1)*n, 0);
                }
            }
        }
        dij();
    }
    return 0;
}

上面那个 建了k+1层,其实只建一层就好了,跟dp一样

dis[i][j]->dis[t][j]+e[i][t] 
dis[i][j]->dis[t][j+1] if j

#include <iostream>
#include <cstring>
#include <cstdio>
#include <algorithm>
#include <queue>
#include <vector>
using namespace std;
const int maxn = 1e4 + 5;
const int maxm = 5e4 + 5;
int n, m, k, s, t, cnt, edgcnt;
int head[maxn], dis[20][maxn], book[20][maxn];
struct node
{
    int to, val, next;
}edge[maxm<<1];
struct Point
{
    int val, to, num;
    Point(){}
    Point(int tt, int vv, int nn) : to(tt), val(vv), num(nn){}
    bool operator < (const Point &a) const
    {
        return val > a.val;
    }
};
void addEdge(int u, int v, int w)
{
    edge[edgcnt].to = v;
    edge[edgcnt].val = w;
    edge[edgcnt].next = head[u];
    head[u] = edgcnt++;
}
void init()
{
    edgcnt = 0;
    memset(head, -1, sizeof(head));
}
void dij()
{
    memset(dis, 0x3f3f3f, sizeof(dis));
    memset(book, 0, sizeof(book));
    priority_queue<Point> q;
    q.push(Point(s, 0, 0));
    dis[0][s] = 0;
    while(!q.empty())
    {
        Point p = q.top();
        q.pop();
        int u = p.to, d = p.val, num = p.num;
        if(book[num][u]) continue;
        book[num][u] = 1;
        if(u == t)
        {
            printf("%d\n", d);
            return;
        }
        for(int i = head[u]; i != -1; i = edge[i].next)
        {
            int to = edge[i].to;
            int w = edge[i].val;
            if(dis[num][u] + w < dis[num][to])
            {
                dis[num][to] = dis[num][u] + w;
                q.push(Point(to, dis[num][to], num));
            }
            if(num == k) continue;
            if(dis[num+1][to] > dis[num][u]) //如果是减少一半就世界+w/2就好了
            {
                dis[num+1][to] = dis[num][u];
                q.push(Point(to, dis[num+1][to], num+1));
            }
        }
    }
//    printf("%d\n", dis[k][t]);
}
int main()
{
    while(~scanf("%d%d%d", &n, &m, &k))
    {
        scanf("%d%d", &s, &t);
        init();
        while(m--)
        {
            int x, y, z;
            scanf("%d%d%d", &x, &y, &z);
            addEdge(x, y, z);
            addEdge(y, x, z);
        }
        dij();
    }
    return 0;
}


HDU 5669 Road(线段树建树(区间连区间))(分层图最短路)


题意:给一个n个点的图,标号为1n,进行m次连边(a,b,c,d,w), 无向图:

K次机会可以消除一条边的权值(即走过但不算),问1n的最短路。

n5×104,m104,0K10,w103

思路:

我们有两颗线段树,一颗连出去,一颗连入,分别称为出线段树和入线段树。出线段树的子节点连到父亲,入线段树的父亲连到子节点。然而如果只是这样的话,我们只能走一条边——从入线段树的某个点走到出线段树的点之后就回不来了!

所以我们把入线段树的每个点连到出线段树的相同位置的点即可。

每次连边是否需要 log2⁡n条边呢?其实不需要,我们可以对每次连边建一个中间节点,出入线段树分别连出,从这个点连入即可。

这样总点数是 4n+m,总边数最大为 2mlog⁡n+6n,所以使用优先队列优化Dijkstra求最短路,总复杂度为 O(Kmlog2⁡n) 。

#include <iostream>
#include <cstring>
#include <cstdio>
#include <vector>
#include <queue>
#include <algorithm>
using namespace std;
typedef long long ll;
const ll inf = 0x3f3f3f3f3f3f3f3fLL;
const int maxn = 1e5 + 7;
const int maxv = maxn*5;
int n, m, s, k;
vector< pair<int, ll> > vt[maxv];
int id[2][maxn<<2], idx;
ll dis[20][maxv];
bool book[20][maxv];
struct node
{
    int to, num;
    ll w;
    node(){}
    node(int tt, ll ww, int nn) : to(tt), w(ww) , num(nn){}
    bool operator < (const node &a)  const
    {
        return w > a.w;
    }
};
void addedge(int u, int v, ll w)
{
    vt[u].push_back(make_pair(v, w));
}
void build(int rt, int l, int r, int type)
{
    id[type][rt] = ++idx;
    if(l == r)
    {
        if(type == 0) addedge(id[type][rt], l, 0);
        else addedge(l, id[type][rt], 0);
        return ;
    }
    int mid = (l+r)>>1;
    build(rt<<1, l, mid, type);
    build(rt<<1|1, mid+1, r, type);
    if(type == 0)  //入树
    {
        addedge(id[type][rt], id[type][rt<<1], 0);
        addedge(id[type][rt], id[type][rt<<1|1], 0);
    }
    else //出树
    {
        addedge(id[type][rt<<1], id[type][rt], 0);
        addedge(id[type][rt<<1|1], id[type][rt], 0);
    }
}
void updatel(int rt, int l, int r, int i, int j, ll cost) //出树,这两个一定要分开,咱是没想到合并的方法
{
    if(i <= l && r <= j)
    {
        addedge(id[1][rt], idx, cost);  //idx是辅助节点, 通过辅助节点连边, 出树权值为cost
        return;
    }
    int mid = (l+r)>>1;
    if(i <= mid) updatel(rt<<1, l, mid, i, j, cost);
    if(j > mid) updatel(rt<<1|1, mid+1, r, i, j, cost);
}
void updater(int rt, int l, int r, int i, int j, ll cost)  //入树
{
    if(i <= l && r <= j)
    {
        addedge(idx, id[0][rt], 0);  //辅助节点连入树相应节点, 入树权值0
        return;
    }
    int mid = (l+r)>>1;
    if(i <= mid) updater(rt<<1, l, mid, i, j, cost);
    if(j > mid) updater(rt<<1|1, mid+1, r, i, j, cost);
}
void dij() //分层图最短路
{
    memset(book, 0, sizeof(book));
    memset(dis, inf, sizeof(dis));
    dis[0][1] = 0;
    priority_queue<node> q;
    q.push(node(1, 0, 0));
    while(!q.empty())
    {
        node p = q.top();
        q.pop();
        int u = p.to, num = p.num;
        ll d = p.w;
        if(book[num][u]) continue;
        book[num][u] = 1;
        for(int i = 0; i < vt[u].size(); i++)
        {
            int to = vt[u][i].first;
            int w = vt[u][i].second;
            if(dis[num][to] > dis[num][u] + w)
            {
                dis[num][to] = dis[num][u] + w;
                q.push(node(to, dis[num][to], num));
            }
            if(num < k && dis[num+1][to] > dis[num][u])
            {
                dis[num+1][to] = dis[num][u];
                q.push(node(to, dis[num+1][to], num+1));
            }
        }
    }
}
int main()
{
    int _;
    cin >> _;
    while(_--)
    {
        for(int i = 0; i < maxn; i++)
            vt[i].clear();
        scanf("%d%d%d", &n, &m, &k);
        idx = n;  //初始n,代表1-n
        build(1, 1, n, 0);
        build(1, 1, n, 1);
        while(m--)
        {
            idx++;
            int l1, l2, r1, r2;
            ll cost;
            scanf("%d%d%d%d%lld", &l1, &r1, &l2, &r2, &cost);
            updatel(1, 1, n, l1, r1, cost);  //无向图, 所以两次
            updater(1, 1, n, l2, r2, 0);
            idx++;  //要新建辅助节点
            updatel(1, 1, n, l2, r2, cost);
            updater(1, 1, n, l1, r1, 0);
        }
        dij();
        if(dis[k][n] == inf)
            puts("CreationAugust is a sb!");
        else
            printf("%lld\n", dis[k][n]);
    }
    return 0;
}

POJ 3635 Full Tank?   无向图最短路+类分层图DP

题意:给出一张图,n<=1000,m<=10000.  有一辆车想从图的一个地方到达另外一个地方,每个点是一个卖油的地方,每个地方买的有价格不一样,车的最大装油量是c,求初始点到终止点的最小花费。

思路:

1.一看到d[i][j]这样的状态表示,马上想到分层图最短路,有点类似DP的思想,按油量分层,d[i][j]表示到节点i还有j个油的最小花费(不是最短路),两种决策,加一个油或者直接走,感觉用Dijkstra写比较好注意状态判重,多判一次也无所谓了

  小优化:离线处理询问,相同起点同时处理
2.设b[i][j]表示在第i个城市,剩余汽油j的答案, 每次选答案最小的去拓展,拓展方案无非两种:买1个汽油或者走向下一个城市, 注意如果新的方案答案比已有答案小,就不再加入优先队列了, 其实出队的节点b[i][j],一定是最优的, 很(一点也不)显然,没有更小的值去更新b[i][j]了,只能用b[i][j]去更新别人, 很想dijkstra, 复杂度O(n*clogn*c)

3.最直接的想法是  每到一个点都加上要走到下一个点所需要的油量。但是走的路不同,到底怎么处理加多少的问题呢?

因此想到分解状态,即拆点。每到一个点都+1单位的油量,然后把这个状态加入队列。另外如果现在油箱内的油足够达到下一点,则更新状态,把新状态加入优先队列。dp[i][j]表示到第i个城市剩余油量为j的花费。

#include <iostream>
#include <cstdio>
#include <cstring>
#include <queue>
#include <vector>
#include <algorithm>
using namespace std;
const int maxn = 1e3 + 5;
const int maxe = 7e4 + 5;
int p[maxn], c, n, m, x, y, z, edgecnt;
int dp[maxn][105], book[maxn][105], head[maxn];
struct Point
{
    int to, w, next;
}edge[maxe];
struct node
{
    int to, w, num;
    node(){}
    node(int tt, int ww, int nn) : to(tt), w(ww), num(nn){}
    bool operator < (const node &a) const
    {
        return w > a.w;
    }
};
void addedge(int u, int v, int w)
{
    edge[edgecnt].to = v;
    edge[edgecnt].w = w;
    edge[edgecnt].next = head[u];
    head[u] = edgecnt++;
}
void init()
{
    edgecnt = 0;
    memset(head, -1, sizeof(head));
}
void dij(int s, int t)
{
    priority_queue<node> q;
    memset(dp, 0x3f3f3f3f, sizeof(dp));
    memset(book, 0, sizeof(book));
    dp[s][0] = 0;
    q.push(node(s, 0, 0));
    while(!q.empty())
    {
        node tmp = q.top();
        q.pop();
        int u = tmp.to, val = tmp.w, num = tmp.num;
        if(u == t)
        {
            printf("%d\n", val);  //这一定是最近的
            return;
        }
        if(book[u][num]) continue;
        book[u][num] = 1;
        for(int i = head[u]; i != -1; i = edge[i].next)
        {
            int to = edge[i].to;
            int w = edge[i].w;
            if(num < c && dp[u][num+1] > dp[u][num] + p[u]) // 留在原地加油,相当于到了下一层图了,只不过这个题是留在原点的下层
            {
                dp[u][num+1] = dp[u][num] + p[u];
                q.push(node(u, dp[u][num+1], num+1));
            }
            if(num >= w && dp[to][num-w] > dp[u][num])  //在这一层到另一个点,主意要判断num >= w
            {
                dp[to][num-w] = dp[u][num];
                q.push(node(to, dp[to][num-w], num-w));
            }
        }
    }
    printf("impossible\n");
    return;
}
int main()
{
    while(~scanf("%d%d", &n, &m))
    {
        init();
        for(int i = 0; i < n; i++)
            scanf("%d", &p[i]);
        while(m--)
        {
            scanf("%d%d%d", &x, &y, &z);
            addedge(x, y, z);
            addedge(y, x, z);
        }
        int q, s, t;
        scanf("%d", &q);
        while(q--)
        {
            scanf("%d%d%d", &c, &s, &t);
            dij(s, t);
        }
    }
    return 0;
}

 

POJ 3249 Test for Job(拓扑排序+dp)


给出一张有向图,每个点有点权,现要从一条入度为0的点到一个出度为0的点,问最大点权和 (有负权)

(1<=n<=1000000,0<=m<=1000000) 

思路:很直观的思路就是 建立超级源超级汇跑一边 spfa就好了,因为有负权,只能spfa,所以这题很容易就被卡死

最坏的情况下,n次spfa,也就是说O(n^2),绝对超时。例如:10000个点(入度为0)都指向一条长度为90000的链的首端,spfa做,
需要9*10^8次。
因为是dag,所以拓扑排序,在DAG上做DP就好了, 复杂度om吧 每条边都被访问一次

#include <iostream>
#include <cstdio>
#include <cstring>
#include <algorithm>
#include <queue>
using namespace std;
const int maxn = 1e5 + 7;
const int maxe = 1e6 + 7;
typedef long long ll;
const ll inf = 1e18;
int n, m, in[maxn], out[maxn], edgecnt, head[maxn];
ll dp[maxn], val[maxn];
struct node
{
    int to, next;
}edge[maxe];
void addedge(int u, int v)
{
    edge[edgecnt].to = v;
    edge[edgecnt].next = head[u];
    head[u] = edgecnt++;
}
int Q[maxn];
void top()
{
    int rear = 0, frnt = 0;
    for(int i = 1; i <= n; i++)
    {
        dp[i] = -inf;
        if(!in[i])
            Q[rear++] = i, dp[i] = val[i];
    }
    while(frnt != rear)
    {
        int u = Q[frnt++];
        for(int i = head[u]; i != -1; i = edge[i].next)
        {
            int to = edge[i].to;
            dp[to] = max(dp[u] + val[to], dp[to]);
            in[to]--;
            if(!in[to]) Q[rear++] = to;
        }
    }
} 
int main()
{
    while(~scanf("%d%d", &n, &m))
    {
        edgecnt = 0;
        memset(head, -1, sizeof(head));
        memset(in, 0, sizeof(in));
        memset(out, 0, sizeof(out));
        for(int i = 1; i <= n; i++)
            scanf("%lld", &val[i]);
        int x, y;
        while(m--)
        {
            scanf("%d%d", &x, &y);
            addedge(x, y);
            in[y]++;
            out[x]++;
        }
        top();
        ll ans = -inf;
        for(int i = 1; i <= n; i++)
        {
            if(!out[i])
            ans = max(ans, dp[i]);
        }
        printf("%lld\n", ans);
    }
    return 0;
}

codeforce 721C Journey (DAG上的dp+记录路径)

题目大意: 

有一个dag图,代表一个城市。城市里有若干景点,以及连接景点的单向道路,每条道路都有一个通过的时间ti。 
你从景点1出发,到景点n,在有限的时间T内,至多能游览几个景点?


思路: DAG一看就跑拓扑。。首先很像dp,就想怎么构造dp呢, 这题的状态一共3个,当前的城市, 当前走了几个, 当前的时间花了多少, 时间一看1e9,知道是前两个作为下标, 也就是dp[i][j]代表当前在第i个城市, 走了j个城市了的最小时间花费。。转移也比较简单 dp[to][j] = dp[u][j-1] + w, 只不过是在DAG上做dp, 一开始写了个裸的dfs(真的zz),t了, 其实拓扑一下就好了, 记得一开始要把所有点都扔进去, 我一开始只扔了1,wa在63,白痴错误。。只把初始状态 dp[1][1] = 0就好了,记录路径比较好, path[to][j]代表到达这个状态的上一个点是什么,j肯定-1就好了, 所以只记录哪个点到达这个状态就好了, 然后倒着找dp[n][j]的最大的j,就是最优状态的最后一个,倒着往前推就好了

#include <iostream>
#include <cstring>
#include <cstdio>
#include <queue>
#include <vector>
#include <algorithm>
using namespace std;
typedef long long ll;
const ll inf = 1e18;
const int maxn = 5e3 + 5;
short path[maxn][maxn], sta[maxn], Q[maxn], deg[maxn], n;
ll val, dp[maxn][maxn];
struct node
{
    int v;
    ll w;
    node(){}
    node(int vv, ll ww) : v(vv), w(ww){}
};
vector<node> vt[maxn];
void Top()
{
    int frnt, rear;
    frnt = rear = 0;
    for(int i = 1; i <= n; i++)
        if(!deg[i])
            Q[rear++] = i;
    while(frnt != rear)
    {
        int u = Q[frnt++];
        for(int i = 0; i < vt[u].size(); i++)
        {
            int to = vt[u][i].v;
            ll w = vt[u][i].w;
            if(--deg[to] == 0) Q[rear++] = to;
            for(int j = 2; j <= n; j++)
            {
                if(dp[to][j] > dp[u][j-1] + w)
                {
                    dp[to][j] = dp[u][j-1] + w;
                    path[to][j] = u;
                }
            }
        }
    }
}
int main()
{
    int m, t, u, v;
    for(int i = 0; i < maxn; i++)
        for(int j = 0; j < maxn; j++)
            dp[i][j] = inf;
    scanf("%d%d%d", &n, &m, &t);
    for(int i = 1; i <= m; i++)
    {
        scanf("%d%d%lld", &u, &v, &val);
        deg[v]++;
        vt[u].push_back(node(v, val));
    }
    dp[1][1] = 0;
    Top();
    int pos = 1, top = 0, index = n;
    for(int i = n; i >= 1; i--)
        if(dp[n][i] <= t)
        {
            pos = i;
            break;
        }
    printf("%d\n", pos);
    sta[top++] = n;
    while(pos >= 2)
    {
        sta[top++] = path[index][pos];
        index = path[index][pos--];
    }
    for(int i = top-1; i >= 0; i--)
        printf("%d%c", sta[i], i == 0 ? '\n' : ' ');
    return 0;
}

51nod 1693 

题意:
那么第一种操作就是num1=num0
第二种操作就是num0+=num1
第三种操作就是num0--
现在bx2k想知道,如果要得到n(1<=n<=10^6)个表情,最少需要几次操作。

思路:

首先简化题目,题面的意思就是,当前有一个数s
操作1是s*=k代价为k,操作2是s--代价为1
求把s从1变到n的最小代价

做法1:

直接暴力记忆化搜索,F(i)表示从1到i用的最小操作数
则F(i)=min(F(i+1),min{F(k)+n/k|n%k==0})
当然这样做肯定过不了
做法2:
考虑把问题转换成图论模型,对于每个i
连边i→i-1边权为1,连边i→i*k边权为k
那么题目就是要求点1到点n的最短路
但是很显然这样复杂度还是很高并不能过
做法3:
注意到如果一条边i→i*k能用若干条边代替
那么i→i*k这条边就是没有意义的
因为那些边的权值的乘积等于k
即那些边的权值和小于等于k
因此对于每个i,只用连i→i*p的边,其中p是质数
这样就可以让边数变得很小了
但是0.1s还是过不去
做法4:
既然过不去,就可以想一些别的办法
考虑在做法3中记录每一个点的最短路路径
就是打一个每个点自己最短路上的上一个点的表
那么可以从表中发现,i→i-1的边不会连续出现4次以上
而且i→i*p的边只有当p<=13的时候才有意义
于是又可以删掉很多边,边数大概是3*10^6条
但是这样做复杂度还是不够,用时稍微多于0.1s
做法5:
既然最短路的做法复杂度太高,就可以再换个思路。
重新考虑记忆化搜索的做法,F(i,j)表示从1到i上一条边的状态是j的最少操作数
j=0时上一条边可以是i→i-1的,否则就是i→i*p的边
那么F(i,0)=min(min{F(i+k,1)|0<k<5},min{F(i/p,0)|p<13})
于是这样做就可以把时间降到最低,从而做出此题

不忘初心 方得始终

PS:后来因为时限问题把时限改成了0.4s,所以做法3就A掉这个题
而且经过我自己的测试,做法5是可以在1s之内通过10^9的数据的
如果大家有兴趣可以试着打一下,毕竟也是一个很好的思路

总结:

在连接i与i*k的边时,只连接k为质数时的点。很容易发现这样是正确的

这也给我们打开了思路,以后遇到乘法的,可以通过质数拆开。

然后是第二个优化,竟然最后k只用到了2,3,5,7,11,13这几个质数。所以被卡题时可以去尝试,看能不能再简化一下数据

另外建边的时候, 如果直接一开始就addedge的话,容易被卡常, 这里每一步的价值都知道了, 那就在spfa里“建边”就好了,另外这题spfa更快

#include <iostream>
#include <cstdio>
#include <cstring>
#include <algorithm>
using namespace std;
const int maxn = 1e6 + 500;
const int INF = 1e9;
int p[10] = {2, 3, 5, 7, 11, 13};
int Q[maxn], dis[maxn], book[maxn], n;
void spfa()
{
    for(int i = 0; i <= n+50; i++)
        dis[i] = INF, book[i] = 0;
    int rear, frnt;
    rear = frnt = 0;
    Q[rear++] = 1;
    book[1] = 1;
    dis[1] = 0;
    while(rear != frnt)
    {
        int u = Q[frnt++];
        if(frnt == maxn) frnt = 0;
        book[u] = 0;
        for(int i = 0; i < 6; i++)
        {
            if(u*p[i] <= n+13 && dis[u*p[i]] > dis[u] + p[i])
            {
                dis[u*p[i]] = dis[u] + p[i];
                if(!book[u*p[i]])
                {
                    Q[rear++] = u*p[i];
                    book[u*p[i]] = 1;
                    if(rear == maxn) rear = 0;
                }
            }
        }
        if(u > 1 && dis[u-1] > dis[u] + 1)
        {
            dis[u-1] = dis[u] + 1;
            if(!book[u-1])
            {
                book[u-1] = 1;
                Q[rear++] = u-1;
                if(rear == maxn) rear = 0;
            }
        }
    }
}
int main()
{
    scanf("%d", &n);
    spfa();
    printf("%d\n", dis[n]);
    return 0;
}



  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值