基础动态规划

基础动态规划

线性动规

最短路径 paths

题目描述

平面内给出 n 个点,记横坐标最小的点为 A,最大的点为 B,现在小 Y 想要知道在 每个点经过一次(A 点两次)的情况下从 A 走到 B,再回到 A 的最短路径。但他是个强 迫症患者,他有许多奇奇怪怪的要求与限制条件:

1.从 A 走到 B 时,只能由横坐标小的点走到大的点。

2.由 B 回到 A 时,只能由横坐标大的点走到小的点。

3.有两个特殊点 b1 和 b2, b1 在 0 到 n-1 的路上,b2 在 n-1 到 0 的路上。

请你帮他解决这个问题助他治疗吧!

输入格式
第一行三个整数 n,b1,b2,( 0 < b1,b2 < n-1 且 b1 <> b2)。n 表示点数,从 0 到 n-1 编 号,b1 和 b2 为两个特殊点的编号。

以下 n 行,每行两个整数 x、y 表示该点的坐标(0 <= x,y <= 2000),从 0 号点顺序 给出。Doctor Gao 为了方便他的治疗,已经将给出的点按 x 增序排好了。

输出格式

输出仅一行,即最短路径长度(精确到小数点后面 2 位)

样例

paths.in

5 1 3
1 3
3 4
4 1
7 5
8 3

paths.out

18.18

样例解释:最短路径:0->1->4->3->2->0

数据范围与提示

20%的数据 n<=20

60%的数据 n<=300

100%的数据 n<=1000

对于所有数据 x,y,b1,b2 如题目描述.


考试的时候一眼就看出来是个dp,死活想不出来,只好写了个二进制搜索,20分;其实dp比二进制短得多

考虑到每个点只能走一次,且从终点往回走和从起点再走一遍到终点没有区别,所以这道题可以转化为求两条不相交路径和的最小值。于是考虑用动态规划求解。

状态转移方程:

F[k][j]=max{F[k][j]F[i][j]+Dis(i,k)

F[i][k]=max{F[i][k]F[i][j]+Dis(j,k)

边界条件: F[0][0]=0

根据这个状态转移方程,然后加上个特判即可。


代码:

#include<iostream>
#include<cstdio>
#include<cstring>
#include<cmath>
#define max(a,b) (a>b?a:b)
#define min(a,b) (a<b?a:b)
using namespace std;

const int MAXN=1000,INF=~0U>>1;

int n,b1,b2;
double dis[MAXN][MAXN],dp[MAXN][MAXN];

struct D{int x,y;} dot[MAXN];
double calDis(D a,D b)
{return sqrt(((double)a.x-b.x)*(a.x-b.x)+((double)a.y-b.y)*((a.y-b.y)));}

int main()
{
    freopen("paths.in","r",stdin);
    freopen("paths.out","w",stdout);
    int i,j,k;
    scanf("%d%d%d",&n,&b1,&b2);
    for(i=0;i<n;i++) scanf("%d%d",&dot[i].x,&dot[i].y);
    for(i=0;i<n;i++)
        for(j=0;j<n;j++)
            if(i!=j) dis[i][j]=calDis(dot[i],dot[j]);
    for(i=0;i<n;i++)
        for(j=0;j<n;j++)
            dp[i][j]=INF;
    dp[0][0]=0;
    for(i=0;i<n;i++)
        for(j=0;j<n;j++)
        {
            if(i&&i==j) continue;
            k=max(i,j)+1;
            if(k==n)
            {
                if(i==n-1) dp[n-1][n-1]=min(dp[n-1][n-1],dp[n-1][j]+dis[j][n-1]);
                else dp[n-1][n-1]=min(dp[n-1][n-1],dp[i][n-1]+dis[i][n-1]);
            }else
            {
                if(k!=b1) dp[k][j]=min(dp[k][j],dp[i][j]+dis[i][k]);
                if(k!=b2) dp[i][k]=min(dp[i][k],dp[i][j]+dis[j][k]);
            }
        }
    printf("%.2lf\n",dp[n-1][n-1]);
    return 0;
}

UVa1025 A Spy in the Metro


某城市的地铁是线性的

思路:

  • 影响到决策的只有当前时间和所处的车站,可以用 f[i][j] 表示当前时刻i,在车站j,还需要等待的时间
  • bool ht[t][i][d]表示在i车站在t时刻向d方向有没有火车(d=0表示向右,1向左)
  • 时间复杂度: O(NT)
  • 可以得出状态转移方程:
    f[i][j]=minf[i+1][j]+1f[i+t[j]][j+1](j<Ni+t[j]<=Tht[i][j][0])f[i+t[j1]][j1](j>1i+t[j1]<=Tht[i][j][1])

#include<iostream>
#include<cstdio>
#include<cstring>
using namespace std;

const int MAXN=50,MAXT=200,INF = 1000000000;
int dp[MAXT+1][MAXN+1];
bool ht[MAXT+1][MAXN+1][2];
int t[MAXN+1];
int casecnt;
int main()
{
    int N,T,M1,M2;
    while(scanf("%d%d",&N,&T)&&N)
    {
        int i,j,x;
        for(i=1;i<N;i++) scanf("%d",&t[i]);

        memset(ht,false,sizeof(ht));
        scanf("%d",&M1);
        while(M1--)
        {
            scanf("%d",&x);
            for(j=1;j<N;j++)
            {
                if(x>T) break;
                ht[x][j][0]=true;x+=t[j];
            }

        }
        scanf("%d",&M2);
        while(M2--)
        {
            scanf("%d",&x);
            for(j=N-1;j>=1;j--) 
            {
                if(x>T) break;
                ht[x][j+1][1]=true;x+=t[j];
            }

        }
        for(int i = 1; i < N; i++) dp[T][i] = INF;
        dp[T][N]=0;
        for(i=T-1;i>=0;i--)
            for(j=1;j<=N;j++)
            {
                dp[i][j]=dp[i+1][j]+1;
                if(j<N && i+t[j]<=T && ht[i][j][0])
                    dp[i][j]=min(dp[i][j],dp[i+t[j]][j+1]);
                if(j>1 && i+t[j-1]<=T && ht[i][j][1])
                    dp[i][j]=min(dp[i][j],dp[i+t[j-1]][j-1]);
            }
        printf("Case Number %d: ",++casecnt);
        if(dp[0][1]<(int)1e9) printf("%d\n",dp[0][1]);
        else printf("impossible\n");
    }
    return 0;
}

小奇挖矿 explo

题目背景

小奇要开采一些矿物,它驾驶着一台带有钻头(初始能力值 w)的飞船,按既定路线依次飞过喵星系的 n 个星球。

问题描述

星球分为 2 类:资源型和维修型。
1.资源型:含矿物质量 a[i],若选择开采,则得到 a[i]p 的金钱,之后钻头损耗 k%,即 p=p(10.01k)
2.维修型:维护费用 b[i],若选择维修,则支付 b[i]p 的金钱,之后钻头修复c%,即 p=p(1+0.01c)
(p 为钻头当前能力值)
注:维修后钻头的能力值可以超过初始值请你帮它决策最大化这个收入

输入格式

第一行 4 个整数 n,k,c,w。
以下 n 行,每行 2 个整数 type,x。
type 为 1 则代表其为资源型星球,x 为其矿物质含量 a[i];
type 为 2 则代表其为维修型星球,x 为其维护费用 b[i];

输出格式

输出一行一个实数(保留两位小数),表示要求的结果。

样例输入

5 50 50 10
110
120
210
220
130

样例输出

375.00

数据范围

对于 30%的数据 n<=100
对于 50%的数据 n<=1000,k=100
对于 100%的数据 n<=100000,0<=k,c,w,a[i],b[i]<=100
保证答案不超过 109


题解上的思路:可以发现,当前的决策只对后面的开采有影响,且剩余耐久度与之后的开采收益成正比,如果倒着考虑这个问题,得出i-n的星球1点耐久度所能获得的最大收益,从后往前dp,得出最大值最后乘w就是答案

神奇的dp,考试时想不到啊


代码超短的

#include<cstdio>
#include<algorithm>
using namespace std;
const int maxn=100001;
int n,w,t[maxn],a[maxn];
double k,c,ans;
int main()
{
    freopen("explo.in","r",stdin);
    freopen("explo.out","w",stdout);
    scanf("%d%lf%lf%d",&n,&k,&c,&w);
    k=1-0.01*k;c=1+0.01*c;
    for(int i=1;i<=n;i++)scanf("%d%d",&t[i],&a[i]);
    for(int i=n;i;i--)
        if(t[i]==1)
            ans=max(ans,ans*k+a[i]);
        else
            ans=max(ans,ans*c-a[i]);
    printf("%.2lf\n",ans*w);
}

线性动规在字符串处理中的应用

[LA3942] Remember the Word


题目描述

Neal 对组合问题十分感兴趣,现在有一道有关文字的问题。Neal 知道 Ray 有着惊人的记忆力,所以他把这道题给了杰杰。

由于杰杰无法准确的记住数字,他用算筹帮助自己。杰杰只被允许使用20071027根算筹,他只能记录下数字模20071027的结果。

题目如下:一个单词应当被分成由数个词典中单词组成的小部分。给出单词和字典,杰杰需要计算有多少种组合方式。

输入格式

输入文件包含多组数据。对于每组数据:第一行为一个长度小于300000的单词。

第二行为一个整数S, 1<=S<=4000

接下来的S行,每一行为字典中的一个单词,其长度小于100。数据保证单词不重复且均为小写。

每组数据之间有空行。

输入文件结束标志为EOF。

输出格式

对每组数据,输出题目描述中要求的数据,注意模20071027

样例输入

abcd
4
a
b
cd
ab

样例输出

Case 1: 2

一道很明显的动规题,代码不难写,主要是想到用字典树优化动规

状态转移方程:

f[i]=sum{f[i+len[x]]xi

#include<cstring>
#include<vector>
using namespace std;

const int maxnode = 4000 * 100 + 10;
const int sigma_size = 26;

// 字母表为全体小写字母的Trie
struct Trie 
{
    int ch[maxnode][sigma_size];
    int val[maxnode];
    int sz; // 结点总数
    void clear() { sz = 1; memset(ch[0], 0, sizeof(ch[0])); } // 初始时只有一个根结点
    int idx(char c) { return c - 'a'; } // 字符c的编号

    // 插入字符串s,附加信息为v。注意v必须非0,因为0代表“本结点不是单词结点”
    void insert(const char *s, int v) 
    {
        int u = 0, n = strlen(s);
        for(int i = 0; i < n; i++) 
        {
            int c = idx(s[i]);
            if(!ch[u][c]) // 结点不存在
            {
                memset(ch[sz], 0, sizeof(ch[sz]));
                val[sz] = 0;  // 中间结点的附加信息为0
                ch[u][c] = sz++; // 新建结点
            }
            u = ch[u][c]; // 往下走
        }
        val[u] = v; // 字符串的最后一个字符的附加信息为v
    }

    // 找字符串s的长度不超过len的前缀
    void find_prefixes(const char *s, int len, vector<int>& ans) 
    {
        int u = 0;
        for(int i = 0; i < len; i++) 
        {
            if(s[i] == '\0') break;
            int c = idx(s[i]);
            if(!ch[u][c]) break;
            u = ch[u][c];
            if(val[u] != 0) ans.push_back(val[u]); // 找到一个前缀
        }
    }
};

#include<cstdio>
const int maxl = 300000 + 10; // 文本串最大长度
const int maxw = 4000 + 10;   // 单词最大个数
const int maxwl = 100 + 10;   // 每个单词最大长度
const int MOD = 20071027;

int d[maxl], len[maxw], S;
char text[maxl], word[maxwl];
Trie trie;

int main() 
{
    int kase = 1;
    while(scanf("%s%d", text, &S) == 2) 
    {
        trie.clear();
        for(int i = 1; i <= S; i++) 
        {
            scanf("%s", word);
            len[i] = strlen(word);
            trie.insert(word, i);
        }
        memset(d, 0, sizeof(d));
        int L = strlen(text);
        d[L] = 1;
        for(int i = L-1; i >= 0; i--) 
        {
            vector<int> p;
            trie.find_prefixes(text+i, L-i, p);
            for(int j = 0; j < p.size(); j++)
                d[i] = (d[i] + d[i+len[p[j]]]) % MOD;
        }
        printf("Case %d: %d\n", kase++, d[0]);
    }
    return 0;
}

最优构造树

题目描述

给出 N 个长度为 L 的单词, N 为 2 的整数次幂,那么我们可以构造出一棵树,把给出的 N 个单词作为树的叶子,其他每个树上的节点也都是一个长度为L的单词,我们定义一条边的代价为它连接的两个节点所代表的单词对位比较不同位的数目,整棵树的代价为所有边的代价和。

示例

上面这棵树的代价和就是 2 。

给出 N 个单词,求最优构造树的最小代价。

输入格式

第一行两个整数 N 和 L , N1024, L1000

接下来 N 行每行一个单词。

多组数据,以0 0结束。

输出格式

一个数表示最小代价。

样例

样例输入

4 3
AAG
AAA
GGA
AGA
0 0

样例输出

3


我们发现一个字符串的各位间互不影响,故可以单独计算。
我们开两个数组:

  • int opt[i][j] 记录 i 节点第 j 位上可以选择的字符,这里使用状压动规,由于题目中的字符串只可能是大写字母,所以可用每个二进制位表示此字符可用与否;
  • int sum[i][j] 记录 i 节点第 j 位上的最小权值。

通过分析,不难得到以下状态转移方程:

  • opt[lc][j]opt[rc][j] 不为 0 ,此时 i 节点的两个儿子在 j 位上相同
    opt[i][j]=opt[lc][j]opt[rc][j]sum[i][j]=sum[lc][j]+sum[rc][j]
  • 否则, i 节点的两个儿子在 j 位上不同,那么所有在两儿子的 opt[c][j] 中出现过的字符都有可能作为 i 节点第 j 位上的字符,且权值加一
    opt[i][j]=opt[lc][j]opt[rc][j]sum[i][j]=sum[lc][j]+sum[rc][j]+1

#include<iostream>
#include<cstdio>
#include<cstring>
using namespace std;

const int MAXN=1030;

int N,L;
int opt[MAXN<<1][MAXN],sum[MAXN<<1][MAXN];

char s[MAXN];

int main()
{
    freopen("tree.in","r",stdin);
    freopen("tree.out","w",stdout);
    int i,j;
    while(scanf("%d%d",&N,&L))
    {
        if((!N)&&(!L)) break;
        memset(opt,0,sizeof(opt));
        memset(sum,0,sizeof(sum));
        for(i=1;i<=N;i++)
        {
            scanf("%s",s+1);
            for(j=1;j<=L;j++)
                opt[i+N-1][j]=1<<(s[j]-'A');
        }
        for(i=N-1;i>=1;i--)
        {
            int lc=i*2,rc=i*2+1;
            for(j=1;j<=L;j++)
            {
                if(opt[lc][j]&opt[rc][j])
                    opt[i][j]=opt[lc][j]&opt[rc][j],
                    sum[i][j]=sum[lc][j]+sum[rc][j];
                else opt[i][j]=opt[lc][j]|opt[rc][j],
                    sum[i][j]=sum[lc][j]+sum[rc][j]+1;
            }
        }
        int ans=0;
        for(i=1;i<=L;i++)
            ans+=sum[1][i];
        printf("%d\n",ans);
    }
    return 0;
}

Market

题目描述

在比特镇一共有 n 家商店,编号依次为 1 到 n。每家商店只会卖一种物品,其中第 i 家商店的物品单价为 ci ,价值为 vi ,且该商店开张的时间为 ti

Byteasar 计划进行 m 次购物,其中第 i 次购物的时间为 Ti ,预算为 Mi 。每次购物的时候,Byteasar会在每家商店购买最多一件物品,当然他也可以选择什么都不买。如果购物的时间早于商店开张的时间,那么显然他无法在这家商店进行购物。

现在 Byteasar 想知道,对于每个计划,他最多能购入总价值多少的物品。请写一个程序,帮助Byteasar 合理安排购物计划。

注意:每次所花金额不得超过预算,预算也不一定要花完,同时预算不能留给其它计划使用。

输入格式

第一行包含两个正整数 n,m,表示商店的总数和计划购物的次数。

接下来 n 行,每行三个正整数 ci,vi,ti ,分别表示每家商店的单价、价值以及开张时间。

接下来 m 行,每行两个正整数 Ti,Mi ,分别表示每个购物计划的时间和预算。

输出格式

输出 m 行,每行一个整数,对于每个计划输出最大可能的价值和。

样例

样例输入

5 2
5 5 4
1 3 1
3 4 3
6 2 2
4 3 2
3 8
5 9

样例输出

10
12

样例解释

第一个计划可以在商店 2,3,5 各购买一件物品,总花费为 1 + 3 + 4 = 8,总价值为 3 + 4 + 3 = 10。

第二个计划可以在商店 1,2,3 各购买一件物品,总花费为 5 + 1 + 3 = 9,总价值为 5 + 3 + 4 = 12。

数据范围与提示

对于 100 的数据, 1ti,Tin

测试点编号nm ci,Mi vi ti,Ti
1 =10 =5 10 10 10
2 =20 =10 100 100 20
3 =100 =1 100 100 =1
4 =200 =1 200 200 =1

3 = 100 = 1 ≤ 100 ≤ 100 = 1
4 = 200 = 1 ≤ 200 ≤ 200 = 1
5 = 150 = 100000 ≤ 150 ≤ 150 ≤ 150
6 = 300 = 100000 ≤ 300 ≤ 300 ≤ 300
7 = 20 = 100000 ≤ 10^9 ≤ 300 ≤ 20
8 = 200 = 100000 ≤ 10^9 ≤ 200 ≤ 200
9 = 300 = 100000 ≤ 10^9 ≤ 300 ≤ 300
10 = 300 = 100000 ≤ 10^9 ≤ 300 ≤ 300

守卫者的挑战 guard

一个赛季有 N 场比赛,给每场比赛的胜率,赢了每场比赛后可能会加 ai 点积分,也可能减去 1 积分,初始有 K 点积分。若积分为非负数且赢了不少于 L 场比赛,则这个赛季是成功的,求成功的概率。

0K2000,0N200,1ai1000,0LN

这种题就是个简单的 DP ,考场上想的太复杂,一直在想概率公式+容斥的做法。

f[i][j][k] 表示:在 i 场比赛,已经赢了 j 场,有 k 点积分的概率,很明显 f[0][0][K]=1 ,那么 ans=Nj=Lf[N][j]

具体实现的时候我们可以将所有积分超过 N 的都算成 N ,因为再大也没有意义,然后第一位可以滚动数组优化一下。

#include<iostream>
#include<cstdio>
#include<cstring>
const int MAXN=201;

int N,L,K;
int a[MAXN];
double p[MAXN],f[2][MAXN][MAXN<<1];

inline void initDP()
{
    int i,j,k;
    f[0][0][std::min(K,N)+200]=1;
    for(i=1;i<=N;i++)
    {
        for(j=0;j<=N;j++)
            for(k=-N;k<=N;k++)
                f[i%2][j][k+200]=0;
        for(j=0;j<=N;j++)
            for(k=-N;k<=N;k++)
            {
                f[i%2][j][k+200]+=f[(i-1)%2][j][k+200]*(1-p[i]);
                f[i%2][j+1][std::min(k+a[i],N)+200]+=f[(i-1)%2][j][k+200]*p[i];
            }
    }
}

int main()
{
    freopen("guard.in","r",stdin);
    freopen("guard.out","w",stdout);
    int i,j,k;
    double ans=0;
    scanf("%d%d%d",&N,&L,&K);
    for(i=1;i<=N;i++) scanf("%lf",&p[i]),p[i]/=100.0;
    for(i=1;i<=N;i++) scanf("%d",&a[i]);
    initDP();
    for(j=L;j<=N;j++)
        for(k=0;k<=N;k++)
            ans+=f[N%2][j][k+200];
    printf("%.6f\n",ans);
    return 0;
}

树上动规

[HAOI2010] 软件安装

题目描述

现在我们的手头有 N 个软件,对于一个软件 i ,它要占用 Wi 的磁盘空间,它的价值为 Vi 。我们希望从中选择一些软件安装到一台磁盘容量为 M 计算机上,使得这些软件的价值尽可能大(即 Vi 的和最大)。

但是现在有个问题:软件之间存在依赖关系,即软件 i 只有在安装了软件 j (包括软件j的直接或间接依赖)的情况下才能正确工作(软件 i 依赖软件 j )。幸运的是,一个软件最多依赖另外一个软件。如果一个软件不能正常工作,那么它能够发挥的作用为 0 。

我们现在知道了软件之间的依赖关系:软件 i 依赖软件 Di 。现在请你设计出一种方案,安装价值尽量大的软件。一个软件只能被安装一次,如果一个软件没有依赖则 Di=0 ,这时只要这个软件安装了,它就能正常工作。

输入格式

第1行: N,M (0N100, 0M500)

第2行: W1,W2,,Wi,,Wn (0WiM)

第3行: V1,V2,,Vi,,Vn (0Vi1000)

第4行: D1,D2,,Di,,Dn (0DiN, Dii)

输出格式

一个整数,代表最大价值

样例

样例输入

3 10
5 5 6
2 3 4
0 1 1

样例输出

5

很明显的是一定要先用 tarjan 缩点,然后再跑树上背包。这里有一个技巧,为了成功地跑树上背包,我们将一个超级源点连接到所有入度为零的点,把森林建成一棵树,这样答案就在源点上了。

#include<iostream>
#include<cstdio>
#include<stack>
using namespace std;

const int MAXN=100,MAXE=10000,MAXM=500;

int M,N;

struct E{int from,to,next;};
struct CFS//Chain Forward Star
{
    E e[MAXE+1];
    int ecnt,G[MAXN+1];
    void addEdge(int u,int v){e[++ecnt]=(E){u,v,G[u]};G[u]=ecnt;}
} G1,G2;

int weight[MAXN+1],value[MAXN+1];

stack<int> st;bool inSt[MAXN+1];
int dfn[MAXN+1],low[MAXN+1],times;
int belong[MAXN+1];
struct S{int w,v;} scc[MAXN+1];int scnt;
void tarjan(int u)
{
    int i;
    dfn[u]=low[u]=++times;
    inSt[u]=true;
    for(i=G1.G[i];i;i=G1.e[i].next)
    {
        int v=G1.e[i].to;
        if(!dfn[v])
        {
            tarjan(v);
            low[u]=min(low[u],low[v]);
        }else
            if(inSt[v])
                low[u]=min(low[u],dfn[v]);
    }
    if(low[u]==dfn[u])
    {
        ++scnt;
        do
        {
            u=st.top();st.pop();
            inSt[u]=false;
            belong[u]=scnt;
            scc[scnt]={weight[u],value[u]};
        }
        while(low[u]!=dfn[u]);
    }
}

bool inDgr[MAXN+1];

inline void rebuild()
{
    int i;
    for(i=1;i<=G1.ecnt;i++)
        if(belong[G1.e[i].from]!=belong[G1.e[i].to])
        {
            G2.addEdge(belongG1.e[i].from],belong[G1.e[i].to]);
            inDgr[belong[G1.e[i].to]]=true;
        }
}

int dp[MAXN+1][MAXM+1];

void dfs(int u)
{
    int i,j,k;
    //dfs过程
    for(i=G2.G[u];i;i=G2.e[i].next)
    {
        dfs(G2.e[i].to);
        for(j=M-scc[u].w;j>=0;j--)
            for(k=0;k<=j;k++)
                dp[u][j]=max(dp[u][j],dp[u][k]+dp[G2.e[i].to][j-k]);
    }
    //更新本节点的值
    for(i=M;i>=0;i--)
        if(i>=scc[u].w)
            dp[u][i]=dp[u][j-scc[u].w]+scc[u].v;
        else dp[u][i]=0;
    //
}

int main()
{
    int i;
    scanf("%d%d",&N,&M);
    for(i=1;i<=N;i++)
        scanf("%d",&weight[i]);
    for(i=1;i<=N;i++)
        scanf("%d",&value[i]);
    for(i=1;i<=N;i++)
    {
        int u=i,v;
        scanf("%d",&v);
        while(v)
        {
            G1.addEdge(u,v);
            scanf("%d",&v);
        }
    }
    for(i=1;i<=n;i++)
        if(!dfn[i]) tarjan(i);
    rebuild();
    //
    for(i=1;i<=scnt;i++)
        if(!inDgr[i])
        {
            inDgr[i]=true;
            G2.addEdge(0,i);
        }
    dfs(0);
    printf("%d\n",dp[0][M]);
    return 0;
}

柠檬当上了警察局长! catch

题目描述

Lemon 因为偶然的原因,当上了警察局长。而一上任,他就碰到了个大麻烦:追捕周克华。周克华是人尽皆知的抢劫杀人犯,而就在几天前,他在 Lemon 辖区内的银行门口,枪杀了一名储户后逃之夭夭。Lemon 知道,如果他抓不住周克华,他的警察局长恐怕就当不下去了。为了能继续当他的警察局长,Lemon 决定倾警察局之物力全力追捕。Lemon 的辖区可以表示为一张边上带权的无向图。银行位于结点 1。

Lemon 仔细研究周克华的案底后得出以下结论: 首先,周克华拥有极强的反侦查能力,因此,他深知不走回头路的重要性。他永远不会访问任何一个结点两次。 其次,周克华深知多走一分钟路就多一分钟暴露的危险,而且他之前已经完全摸清了辖区的地形,因此他总是走最短路,也就是,他访问任何一个结点时,走的路线都是从银行到这里的最短路。为了简化题目,我们保证从银行(结点 1)到任何一个结点的最短路都是唯一的。 再次,周克华知道,为了尽可能远离案发现场,他必须不停的运动。也就是说,只要有相邻的结点能满足“不走回头路、只走最短路”的前提,他一定会移动。如果有多个相邻结点可供选择,他会随机等概率选择一个作为他的移动目标。如果没有结点满足这一要求,那么周克华就会选择遁入深山之中,而可以想象在距离案发现场十万八千里的山区里抓捕周克华的难度,所以一旦周克华遁入山中,也就意味着 Lemon 的抓捕行动失败了。

Lemon 分析出了以上结论后决定,只能在结点上布置警察,实施埋伏抓捕。但是,周克华的身体素质、反侦查能力和使用武器技术都十分优秀,因此,即使周克华遇到了埋伏,也有一定几率杀害所有参与埋伏的警察后逃脱。当然,随着埋伏的警察的数目的增多,逃脱几率会减小。如果逃脱成功,周克华会像什么都没发生一样,继续按上文所述的方式行动。

注意,周克华一旦到达一个结点,埋伏在那里的警察会立即实施抓捕,只有周克华逃脱了在当前结点的抓捕后才能进行下一步行动(遁入群山或继续移动),包括结点 1,也就是周克华需要先逃脱结点 1 的埋伏才能走出他的第一步。

Lemon 知道,他的设置警力方式决定了追捕成功的概率。他现在已经知道了他的辖区地图以及在不同地点设置不同数量的警力能成功抓捕周克华的概率,Lemon 现在想要找到一个尽量优的方式设置警力,因此求助于你。你能告诉 Lemon 在最优的设置下,抓捕成功概率是多少吗? Lemon 到时或许会把高额的悬赏分给你一部分的哦~

输入格式

输入文件第一行包含两个数 N,M,分别表示辖区里的结点数目和边的数目。

接下来 M 行,每行 3 个数 a,b,c,表示结点 a 和 b 之间有一条权值为 c 的无向边。

接下来一个数 S,表示可以参与埋伏的警察个数。

接下来 N 行,每行 S 个数,第 i 行第 j 个数 Pij 表示在结点 i 埋伏 j 个警察抓捕成功的概率。

注意,如果不埋伏任何警察,那么显然绝不可能成功抓住周克华。

输出格式

输出文件仅包含一个实数,保留到 4 位小数,表示在最优警力设置下,抓捕成功的概率。

样例

输入样例

4 4
1 2 1
1 3 2
2 4 3
3 4 1
2
0.01 0.1
0.5 0.8
0.5 0.8
0.7 0.9

输出样例

0.6000

地图如下:

地图

括号内的数为权值。

周克华在结点 1 会等概率选择 2 或 3 逃跑。如果选择了 2,那么下一步他会选择遁入群山,因为 1 已经访问过了,而继续往 4 走就不是最短路了(1=>3=>4 比 1=>2=>4 短);如果选择了 3,那么下一步他会继续往 4 跑,然后选择遁入群山(到达 4 后继续往 2 跑也不是最短路)。

最优警力设置是,在 2、4 处各设置一名警察。这样如果周克华在第一步选择了 2 (50%概率),那么在 2 处有 50%概率被抓,如没有被成功抓住则遁入群山。如果第一步选择了 3(50%概率),在 3 处被抓概率为 0(因为没有警察埋伏),但接下来周克华会往 4 走(100%概率),在 4 处被抓的概率是 70% 所以总成功率是 50%×50%+50%×70%=0.6

数据范围与提示

时间限制为 1s

对于 20% 数据,满足 N,S6 .

对于 50% 数据,满足 N,S30 ,每个结点度数不超过 3.

对于 100% 数据,满足 S200,M20000,1a,bN,1c10000,0<Pij1 .

数据保证图中没有自环或重边,从结点 1 到任何一个结点的最短路唯一。


这道题令我十分开心,考试的时候居然想出正解了,只可惜时间不够没调对,得了 20 分,不过考试之后按照原来的思路 AC 了。

看完题目很容易想到先用最短路算法建一个生成树。

之后树上动规,对每个节点背包。

我们开一个数组 int dp[u][n] 表示 u 节点有 n 个人可用时最大的概率,从树的底往树顶更新。对于一个未知节点,它的儿子一定已知,我们把不同人数情况下所对应的子节点作为不同的物品,将当前剩余的人数作为容量,每个物品的花费即为人数,据此写 01背包。枚举此未知节点的人数,分别计算结果,取最大结果记录在 dp[u][n] 中。

边界条件: dp[u][0]=0dp[u][n]=P[u][n]u is leaf node


#include<iostream>
#include<cstdio>
#include<queue>
#include<cstring>
#define max(a,b) (a>b?a:b)
using namespace std;

const int MAXN=205,MAXM=20005,INF=~0U>>1;

int N,M,S;
struct E{int next,from,to,value;};
struct CFS
{
    E e[MAXM];
    int ecnt,G[MAXN];
    void addEdge(int u,int v,int w)
    {e[++ecnt]=(E){G[u],u,v,w};G[u]=ecnt;}
} G,T;

struct HN
{
    int id,v,eid;
    bool operator <(const HN & ot)const
    {return v>ot.v;}
};

priority_queue<HN> heap;

int outdgr[MAXN];

int dis[MAXN];bool vis[MAXN];
void dijkstra()
{
    int i;
    for(i=1;i<=N;i++) dis[i]=INF;
    dis[1]=0;
    heap.push((HN){1,0,0});
    while(!heap.empty())
    {
        HN rn=heap.top();heap.pop();
        int u=rn.id;
        if(vis[u]) continue;
        vis[u]=true;
        if(rn.eid)
        {
            E te=G.e[rn.eid];
            T.addEdge(te.from,te.to,te.value);
            outdgr[te.from]++;
        }
        for(i=G.G[u];i;i=G.e[i].next)
        {
            int v=G.e[i].to;
            if(vis[v]) continue;
            if(dis[v]>dis[u]+G.e[i].value)
            {
                dis[v]=dis[u]+G.e[i].value;
                heap.push((HN){v,dis[v],i});
            }
        }
    }
}

double P[MAXN][MAXN],dp[MAXN][MAXN];

int chN[MAXN],tmpcnt;
double bb[MAXN];
double calbb(int lv)
{
    memset(bb,0,sizeof(bb));
    int i,j,k;
    for(i=1;i<=tmpcnt;i++)
        for(j=lv;j>=0;j--)
            for(k=0;k<=j;k++)
                bb[j]=max(bb[j],bb[j-k]+dp[chN[i]][k]);
    return bb[lv];
}

int dfn[MAXN],maxDfn;
int ceng[MAXN][MAXN],ccnt[MAXN];
void calDfn(int u,int d)
{
    dfn[u]=d;ceng[d][++ccnt[d]]=u;
    if(d>maxDfn) maxDfn=d;
    if(!outdgr[u]) return;
    int i;
    for(i=T.G[u];i;i=T.e[i].next)
    {
        int v=T.e[i].to;
        calDfn(v,d+1);
    }
}

int main()
{
    freopen("catch.in","r",stdin);
    freopen("catch.out","w",stdout);
    int i,j,k;
    scanf("%d%d",&N,&M);
    for(i=1;i<=M;i++)
    {
        int u,v,w;scanf("%d%d%d",&u,&v,&w);
        G.addEdge(u,v,w);
        G.addEdge(v,u,w);
    }
    dijkstra();
    scanf("%d",&S);
    for(i=1;i<=N;i++)
        for(j=1;j<=S;j++)
            dp[i][j]=-1;
    for(i=1;i<=N;i++)
        for(j=1;j<=S;j++)
            scanf("%lf",&P[i][j]);
    calDfn(1,1);
    int d,c;
    for(c=maxDfn;c>=1;c--)
        for(d=1;d<=ccnt[c];d++)
        {
            int u=ceng[c][d];
            dp[u][0]=0;
            if(!outdgr[u])
                for(i=1;i<=S;i++)
                    dp[u][i]=P[u][i];
            else
            {
                tmpcnt=0;
                for(j=T.G[u];j;j=T.e[j].next)
                    chN[++tmpcnt]=T.e[j].to;
                calbb(S);
                for(i=1;i<=S;i++)
                {
                    double res=0;
                    for(int rn=0;rn<=i;rn++)
                    {
                        double rv=bb[i-rn];
                        rv/=outdgr[u];
                        rv=P[u][rn]+(1-P[u][rn])*rv;
                        if(rv>res) res=rv;
                    }
                    dp[u][i]=res;
                }
            }
        }
    printf("%.4lf\n",dp[1][S]);
    return 0;
}

区间动规

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值