退役前的简单dp训练

今年应该就是最后一年的比赛了,毕竟大三狗了。学了快三年,博客这种东西写一篇少一篇。 退役前可能还能自我训练几次,不多了。。。。
http://blog.csdn.net/column/details/codeforces-dp.html?&page=2
多谢黎晨dalao,学习您的这个博客是我受益匪浅。

简单dp:
codeforces 494B B. Obsessive String
http://codeforces.com/contest/494/problem/B

这题还挺难的。。。
给一个串s 和 模式串t
求合法集合的多少;
合法集合: 有一个过多个s的子串构成, 子串不能重叠,子串必须包含模式穿t。

比如ababa: =5 t=aba
{aba}{abab}{ababa}{baba}{aba}

那如果 abaaba t=aba
{aba}{abaa}{abaab}{abaaba}{baaba}{aaba}{aba}
以及:
{aba aba}

我们试着解释一下第三组样例:
d1d2d3
d
=12
d3:{d3} dp[3]=1

d2:{d2} {d2d3} + {d2 + d3} dp[2]=2 + 1 = 3

d1:{d1} {d1d2} {d1d2d3} + {d1+d2} {d1+d2d3} {d1+ d2 +d3 } {d1+d3} +{d1d2 +d3}

dp[3] = 3 + 3*1 + 1*2=8
=12
如果照这么理解: dp[i] = dp[i+2]*2 为什么呢?
因为 我们已 {模式串t1 + dp[i+2]} 也可以 {模式串t1 i+1 + dp[i+2] }

同理如果是i+3 可以 {i + dp[i+3]} { ii+1 +dp[i+3] } {ii+1i+2 + dp[i+3]} 所以应该*3

首先我们要能够确定在 s串中t出现的位置。

设 dp[i] :以i 开头的往后的有多少个

所以
如果 i 不是匹配模式串 的开头
dp[i]=dp[i+1]; 相当于把当前i 加给后面 dp[i+1]代表的 合法集合
如果是:
dp[i]=n-(i+len2-1)+
dp[i+len2] //集合: 第一个模式串 + 以i+len2 …开头往后的合法集合
dp[i+len2+1]*2 // : 第一个模式串 + 以i+len2+1 开头的合法集合数量


const ll mod=1e9+7;

char pattern[200005],text[200005];   //pattern 模式串  ,text是主串,nexxt是处理之后的跳转 都是从0开始保存!
__int64 nexxt[200005];
int matches[200005];
__int64 ans;
int len1,len2;
void BuildNext() //KMP算法中所用到的跳转表next,是算法的核心问题
{
    len2=strlen(pattern);
    int i, t;
    i = 1;
    t = 0;
    nexxt[1] = 0;
    while(i < len2+1 ){                    // t-表示-f(j)
        while(t > 0 && pattern[i - 1] != pattern[t - 1]){
            t = nexxt[t];
        }
        ++t;
        ++i;
        if(pattern[i - 1] == pattern[t - 1]){
            nexxt[i] = nexxt[t];
        }
        else{
            nexxt[i] = t;
        }
    }
    //pattern末尾的结束符控制,用于寻找目标字符串中的所有匹配结果用
    while(t > 0 && pattern[i - 1] != pattern[t - 1]){
        t = nexxt[t];
    }
    ++t;
    ++i;
    nexxt[i] = t;
}

__int64 KMP()
{
    int i, j;
    __int64 ans;
    BuildNext();
    len1=strlen(text);
    i = 0;
    j = 1;
    ans = 0;
    while(len2 +1 - j <= len1 - i){
//      printf("%d %d :%c %c\n",i,j-1,text[i],pattern[j - 1]);
        if(text[i] == pattern[j - 1] ){
            ++i;
            ++j;
            //发现匹配结果,将匹配子串的位置,加入结果
            if(j == len2+1 ){
                matches[ans++] = i-len2;
                j = nexxt[j];
            }
        }
        else{
            j = nexxt[j];
            if(j == 0){
                ++i;
                ++j;
            }
        }
    }
    //返回发现的匹配数
    return ans;
}
int flag[200005];
ll dp[200005];
ll sum[200005];
ll sum_dp[200005];
// sum[i] 应该 = i + i+1 * 2 + i+2 *3 + i+3 * 4.... sum[i+1]= i+1 + i+2 *2 + ...
// sum[i] = dp[i] + sum[i+1] + dp[i+1] + dp[i+2] +......;
int main(){
  //  freopen("1.txt","r",stdin);
    scanf("%s",text);
    scanf("%s",pattern);
    int L=KMP();
    memset(flag,0,sizeof(flag));
    for(int i=0;i<L;i++){
//      printf("mat=%d\n",matches[i]);
        flag[matches[i]]=1;
    }
    ll ans=0;
    dp[len1]=0;
    sum[len1]=0;
    sum_dp[len1]=0;
    for(int i=len1-1;i>=0;i--){
        if(flag[i]==0){
            dp[i]=dp[i+1];
        }
        else{
            dp[i]= (len1-1) - (i+ len2 -1) +1;
            dp[i]+= sum[i+len2] ;  // i+len2 + i+len2+1 *2 + i
            dp[i]%=mod;
        }

        sum[i]=(dp[i]+ sum[i+1]+sum_dp[i+1])%mod;
        sum_dp[i]=(sum_dp[i+1] + dp[i])%mod;
        ans=(ans+dp[i])%mod;
//      printf("i=%d  sum=%I64d %I64d    %I64d %I64d \n",i,sum_dp[i],sum[i],dp[i],ans);
    }
    printf("%I64d\n",ans);

    return 0;
}

codeforces 571B B. Minimization
给一个数组a 3*1e5 和一个数k <5000
可以随便改变这些元素的顺序,也可以不改变
使得 abs(a[1]-a[1+k]) + abs(a[2]-a[2+k]) …+ abs(a[n-k]-a[n]) 最小

整个数组可以看成
[..k..] [.k..] [..k..] [..n%k..]
= [..n%k] [..k..] [..k..] [..k..]
= [ .n%k.+ k-n%k..] [..k..] [..k..] [.n%k..]
考虑长度为 n/k + 1的链 有 n%k 条
那么剩下的长度为 n/k的链 有 k - n%k条
{两种已经分完了所有的数。仔细读读题意就可以发现了}

对于某条链,我们从小到大排 且是连续的数构成, 最小的差值就是max-min
我们分成两种链 就好了
其中长度为 n/k + 1的链 有 n%k 条,长度为 n/k的链 有 k - n%k条
我们不确定的是: 按什么样的顺序分配给 链1 链2 可以获得最小值,那么我们就可以开始dp了,于是看起来变成了简单dp。

我们设dp[i][j] 表示第一种链为i第二种链为j 的最小值

dp[i][j] = min dp[i-1][j] + a[ i*l1+j*l2 ]-a[ i*l1+j*l2-l1 +1 ]
dp[i][j-1] + a[ i*l1+j*l2 ]-a[ i*l1+j*l2-l2 +1 ]

#define ll __int64
ll dp[5005][5005];
int a[300005];
int main(){
   // freopen("1.txt","r",stdin);
    int n,k;
    scanf("%d %d",&n,&k);
    for(int i=1;i<=n;i++)
        scanf("%d",&a[i]);
    sort(a+1,a+n+1);
    int l1=n%k;
    int l2=k-n%k;
    int len1=n/k+1;
    int len2=n/k;
    memset(dp,63,sizeof(dp));  // 这样可以直接复制最大值?? 学到了
    dp[0][0]=0;
    for(int i=0;i<=l1;i++)  //
        for(int j=0;j<=l2;j++){
//          printf("%I64d\n",dp[i][j]);
            int endd=i*len1+j*len2;
            if(i>0) dp[i][j]=min(dp[i][j],dp[i-1][j] + a[endd]-a[endd-len1+1]);
            if(j>0) dp[i][j]=min(dp[i][j],dp[i][j-1] + a[endd]-a[endd-len2+1]);
        }
    printf("%I64d\n",dp[l1][l2]);
return 0;
}

codeforces 583B B. Once Again…
http://codeforces.com/contest/583/problem/D
给一个数组a
n*T
i>n ai=ai-n=a[i%n]
找到最长 非递减数列

分析:
XJB想了一下午,其实最开始的想法就是对的
暴力跑出 n*n的 LIS
然后把 t-n 插入LIS中就行了。

#define ll __int64
int dp[100005];
int a[100005];
int b[100005];

int main(){
   // freopen("1.txt","r",stdin);
    int n,t;
    map<int,int>mp;
    int maxn=0;
    scanf("%d %d",&n,&t);

    for(int i=1;i<=n;i++){
        scanf("%d",&a[i]);
        mp[a[i]]++;
        maxn=max(maxn,mp[a[i]]);
   }
    int L=0;
    int temp=t;
    if(temp>n){
        temp=n;
    }
    for(int j=1;j<=temp;j++)
        for(int i=1;i<=n;i++)
            b[++L]=a[i];
    int len=1;
    dp[1]=b[1];
    for(int i=2;i<=L;i++){
        int pos=upper_bound(dp+1,dp+1+len ,b[i])-dp;
        if(pos>len)
            dp[++len]=b[i];
        else
            dp[pos]=b[i];

    }
    if(t>n)
        len+=(t-n)*maxn;
    printf("%d\n",len);

return 0;
}

codeforces 158E. Phone Talks(dp)
http://codeforces.com/contest/158/problem/E

有n个电话 要拨打
每个电话有:开始的时间ti , 和持续的时长di
所有的ti不相同,

可以忽视掉 最多K 个电话,可以推到第二天甚至以后

问:当天最多可以连续睡多少时间 [1,86400]
忽略掉的K 个电话肯定是连续的
感觉 不用dp啊,n*n 能跑过去吧

还是 naive了,有时候不能去掉连续的。。。因为持续时间的不同
比如
[1,10] [2,5][30,1] [100,100]; 去掉两个 去掉 2 3 和1 2 就不如去掉1 3

那么我们就只能想想dp了
设 dp[i][j] 假设前i个电话里面 推 k-j个电话 还剩j个的最长休息时间。
我们会发现方程无法写出,因为无法知道dp[i-1] 里面到底推掉了那些电话。

设:dp[i][j]表示 接完第i个电话且推掉j个电话 的 最少需要的时间。

dp[i][j] =
if(dp[i-1][j] > f[i].sta) dp[i][j]= dp[i-1][j]+f[i].d;
else dp[i][j] = f[i].end;

dp[i][j]=min(dp[i][j], dp[i-1][j-1]);

我们知道了完成前i个电话,推掉j个的最少时间

那么我们就可以
for(int i=1;)..
for(int j=0;j….)
推掉 dp[i][j] 前i个电话里面的j个最快完成时间
然后把 i 后面连续的 k-j 的电话都推掉,求出时间间隔。

似乎是正确的

struct node{
    int sta,d,to;
    bool operator < (node b)const{
        return sta<b.sta;
    }
}f[4005];
int dp[4005][4005];
 int main() {
     int n,k;
     scanf("%d %d",&n,&k);
     for(int i=1;i<=n;i++){
         scanf("%d %d",&f[i].sta,&f[i].d);
         f[i].to=f[i].sta+f[i].d;
     }
     if(k==n){
        printf("86400\n");
        return 0;
     }
     sort(f+1,f+n+1);
     dp[0][0]=1;
     dp[0][1]=1;
     dp[1][0]=f[1].to;

     for(int i=1;i<=n;i++){
         for(int j=0;j<=k;j++){
             if(j==i){
                 dp[i][j]=1;
                 break;
             }
             if(j>=i-1 || dp[i-1][j]<=f[i].sta)
                 dp[i][j]=f[i].to;
             else if(dp[i-1][j] > f[i].sta)
                 dp[i][j]= dp[i-1][j]+f[i].d;

             if(j>0)
                 dp[i][j]=min(dp[i][j], dp[i-1][j-1]);
         }
     }

     int ans=0;
     for(int i=1;i<=n;i++){   // 假设i前面
         for(int j=0;j<=k;j++){  // 前i个推掉j 个,假设前i+(k-j) 个推掉了k个,即吧i 后面的连续的全部推掉
             ans=max(ans,f[i+1].sta-dp[i][j]);
             if(i+k-j+1>n)
                 ans=max(ans,86401-dp[i][j]);
             else
                 ans=max(ans,f[i+k-j+1].sta-dp[i][j]);
         }
     }

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

codeforces 163A Substring and Subsequence(dp)
http://codeforces.com/contest/163/problem/A
x 是第一个字符串的 子串 len<5000
y 是第二个字符串的 子序列 len<5000

问x=y的 有多少对 允许n*n的时间复杂度

设 dp[i][j] 为第一个字符串前i个子串 和第二 的前j个子序列 相同的有多少个

if i==j
那么dp[i][j] =
dp[i-1][j-1] :
1….i-1 i
1….j-1 j

aaa
aaa
dp[1][1]=dp[0][0]+1=1; dp[1][2]=dp[1][1]+1=2 ; dp[1][3]=dp[1][2]+1=3;
dp[2][1]=dp[1][1]+1=2 dp[2][2]=dp[1][1]+dp[1][2]+dp[2][1]-dp[1][1] +1 =5
dp[2][3] = dp[1][3] + dp[2][2] +1 = 8+1=9 (-dp[1][1] +dp[1][1])

dp[3][1]=dp[2][1]+1=3;

dp[3][2]=8 = dp[3][1]+ dp[2][2] +1 - dp[3-2][1]=8; 因为 1 -3 必须是连续的子串
dp[3][3]=16= dp[3][2]+dp[2][3]+1 =8+9+1 - dp[1][2]=16;

好像 对于i,j
有dp[i][j]= dp[i][j-1] + dp[i-1][j] +1 - (dp[i-2][j-1]) ;

因为括号部分是重复部分,而题目要求 a串中选出的必须是子串,所以1

const ll mod=1e9+7;
char s1[5005];
char s2[5005];
ll dp[5005][5005];
int main() {
    // freopen("1.txt","r",stdin);
     int n,k;
     scanf("%s %s",s1+1,s2+1);
     int l1=strlen(s1+1);
     int l2=strlen(s2+1);
     memset(dp,0,sizeof(dp));
     for(int i=1;i<=l1;i++)
         for(int j=1;j<=l2;j++){
             if(s1[i]==s2[j]){
                 if(i>2)
                     dp[i][j]= ( (dp[i][j-1] + dp[i-1][j])%mod + mod - dp[i-2][j-1])%mod;
                 else
                     dp[i][j]= dp[i][j-1] + dp[i-1][j] ;
                 dp[i][j]++;
             }
             else
                    dp[i][j]=( (dp[i-1][j] +dp[i][j-1])%mod + mod -dp[i-1][j-1])%mod;
             dp[i][j]%=mod;
//           printf("%d %d =%I64d\n",i,j,dp[i][j]);
         }
     printf("%I64d\n",dp[l1][l2]);
    return 0;
 }

codeforces 180C C. Letter(dp)

区间dp:
codeforces 245H H. Queries for Number of Palindromes(区间dp)

概率dp:
codeforces 351B B. Jeff and Furik(概率)

树形dp:
codeforces 486D D. Valid Sets ;http://codeforces.com/contest/486/problem/D
题意:
给一颗树,每个点上有值ai
找到 多少个 连通子树 且 其中最大值-最小值<=d

从时间复杂度上看 我们可以采用n*n

也就是说可以把每一个点当成根,扫一遍,得出所有 !包含root! 的满足条件的子树个数。

我们对于每个根,把它当成最大值,遇到比他大的我们就中断
只考虑比它小的子树,而且必须满足a[root]-a[i]<=d,否则也中断
那么考虑 u->v
dp[v]=x 那么dp[u]:代表 从root - u 都满足条件数量=dp[u],那么
dp[v]的含义 本来就是从 包含root-v的前提下,v 下面的子树可以构成 x 个满足条件的,
那么dp[u] 的新增的数量 就是dp[u]*dp[v];

这样还是会有重复值
比如 3-5 和 5-3 可能就产生重复值,所以我们规定只能从3-5 或者只能从5-3 就好了,这种解法真是机智啊。

int a[N];
vector<int >G[N];
ll dp[N];
int n,d;
void dfs(int u,int pre,int rot){
    for(int i=0;i<G[u].size();i++){
        int v=G[u][i];
        if(v==pre) continue;
        if(a[v]>a[rot]) continue;
        if(a[rot]-a[v]>d) continue;
        if(a[rot]==a[v] && v>rot) continue;  //定义只能从  rot->v  即rot<v
         //满足了 所有条件之后,说明仅v 贡献值为1
        dp[v]=1;
        dfs(v,u,rot);
//      printf("i=%d u-v %d %d %I64d\n",i,u,v,dp[v]);
        dp[u]+=dp[u]*dp[v];
        dp[u]=dp[u]%mod;
    }

}
int main(){
    //freopen("1.txt","r",stdin);
    scanf("%d %d",&d,&n);
    for(int i=1;i<=n;i++)
        scanf("%d",&a[i]);
    int a,b;
    for(int i=1;i<n;i++){
        scanf("%d %d",&a,&b);
        G[a].push_back(b);
        G[b].push_back(a);
    }
    ll ans=0;
    for(int i=1;i<=n;i++){
        dp[i]=1;
        dfs(i,0,i);
        ans+=dp[i];
        ans=ans%mod;
    }
    printf("%I64d\n",ans);

return 0;
}

codeforces 337D D. Book of Evil(树形dp)
题意:给一棵树
两个点的距离= 经过的边的条数
有一个污染源,能污染距离<=d的点。
现给你一些被污染的点。 其他点不知是否被污染了。
问:污染源 有可能出现在几个地点。

n,m=1e5
所以n*n的方法被否决。

有一个基本的想法: dfs一遍,把所有污染源能够污染的点+1
On扫一遍得到所有污染数=m的点。

因为如果dfs,那么有些点能够影响祖先、兄弟。。。。显然难以处理,需要把每个点当成根来做。 O(n*m)

然后我们可以用dp 来做一做(别问为什么,看的题解)

dp[i]代表某个点到 离它最远的确定的感染点 的距离。
因为树:
我们可以知道每个点i 到 他的子树中的感染点的最远距离
问题2:如何得到每个点 除了自己、自己的子树之外的 最远感染点距离。

我们可以知道每个点的 所有儿子节点的 son[u]
那么 son[1] ..son[2].. son[i+1]….
对于1 来说fa[1]=max(son[2]….son[x]) if(存在) + 1;
//那么其实只要维护最大值,和次大值即可

using namespace std;
#define ll __int64
/*
5 4 1
2 3 4 5

1 2
2 3
2 4
1 5
 */
#define N 100005
struct node{
    int to,d;
    int next;
}edge[N*2];
int head[N],tot;
void addedge(int u,int v){
    edge[tot].to=v;
    edge[tot].next=head[u];
    head[u]=tot++;
}

int p[N];
int son[N],fa[N];
int flag[N];
void init(){
    memset(flag,0,sizeof(flag));
    memset(head,-1,sizeof(head));
    tot=0;
    memset(son,-31,sizeof(son));
    memset(fa,-1,sizeof(fa));
}
void dfs(int u,int pre,int l){  // 得到子树中最大的距离
//  printf("u=%d  p=%d\n",u,pre);
    if(flag[u]==1)
         son[u]=0;
    for(int i=head[u];i!=-1;i=edge[i].next){
        int to=edge[i].to;
        if(to==pre) continue;
//      printf("u=%d %d\n",u,to);
        dfs(to,u,l+1);
        son[u]=max(son[u],son[to]+1);
    }
}
void dfs2(int u,int pre){
    if(flag[pre]==1)   //这里显然是错的,肯定不能直接覆盖
        fa[u]=max(fa[u],1);
    if(fa[pre]>-1)
        fa[u]=max(fa[u],fa[pre]+1);
//  printf("dfs2 :%d %d %d \n",u,pre,fa[u]);
    int maxn=-1,minn=-1;
    int p1;
    for(int i=head[u];i!=-1;i=edge[i].next){
        int to=edge[i].to;
        if(to==pre) continue;
//      printf("u to :%d %d %d\n",u,to,son[to]);
        // 维护方法有问题啊。这样会覆盖掉次大值,比如 2 .3  先进入2 覆盖maxn,然后3也会覆盖maxn
        if(son[to] > minn){  //肯定要先和次大值比,在分情况讨论
            if(son[to]>maxn){
                minn=maxn;
                maxn=son[to];
                p1=to;
            }
            else{
                minn=son[to];
            }
        }
    }
//  if(u==1)
//      printf(" %d %d %d\n",maxn,minn,p1);
    for(int i=head[u];i!=-1;i=edge[i].next){
        int to=edge[i].to;
        if(to==pre) continue;
        if(fa[u]>-1)
            fa[to]=max(fa[to],fa[u]+1);
        if(to==p1){
            if(minn>-1)
                fa[to]=max(fa[to],minn+2);
        }
        else{
            if(maxn>-1)
                fa[to]=max(fa[to],maxn+2);
        }
//      if(u==1)
//          printf("u=fa[v]= %d %d %d \n",u,to,fa[to]);
        dfs2(to,u);
    }
}
int main() {
    int n,m,d;
    init();
    scanf("%d %d %d",&n,&m,&d);
    for(int i=1;i<=m;i++){
        scanf("%d",&p[i] );
        flag[p[i]]=1;
    }
    int u,v;
    for(int i=1;i<n;i++){
        scanf("%d%d",&u,&v);
        addedge(u,v);
        addedge(v,u);
    }
    dfs(1,0,0);
    dfs2(1,0);
    int ans=0;
    for(int i=1;i<=n;i++){
//      printf("%d %d %d\n",i,son[i],fa[i]);
        if(son[i]>d  || fa[i]>d)
            ans++;
    }
    printf("%d\n",n-ans);

    return 0;
}

codeforces 461B B. Appleman and Tree(树形dp)
http://codeforces.com/contest/461/problem/B
给一棵树,n个定点,每个点非黑即白
问有多少种方法将去掉一些边 k条
之后形成k+1 个连通块,使得每个连通块里面仅有1个黑点

首先我们很容易能想到一种方法:把每个黑点和父亲的边删去。

用树形dp 的思想:
设:
dp[i][2]
dp[i][0]表示i包含i的子树 连通块无黑点的方案数,1表示有黑点的方案书
所以如果一个点是黑色dp[i][1]=1
如果一个点是白色dp[i][0]=1

我们考虑,在已知根的情况下,添加子树

1.如果当前根i 有黑点了:
添加的子树树根有黑点: 这条边只能分割; 结果: dp[i][1] * dp[v][1]
添加的子树没有黑点: 这条边只能保留 结果: dp[i][1]*dp[v][0]
2.如果当前根i 没有黑点:
添加的子树有黑点 :
这条边保留:dp[i][0] * dp[v][1]
这条边分割:dp[i][1] * dp[v][1]
添加的子树无黑点
这条边只能保留:dp[i][0] * dp[v][0]

说实话我觉得dp的转移有点莫名其妙就过了,正确性真是。。。

using namespace std;
#define ll __int64
/*
 */
const ll mod=1e9+7;

#define N 100005
struct node{
    int to,d;
    int next;
}edge[N*2];
int head[N],tot;
int col[N];
void addedge(int u,int v){
    edge[tot].to=v;
    edge[tot].next=head[u];
    head[u]=tot++;
}

ll dp[N][2];
void init(){
    memset(head,-1,sizeof(head));
    memset(dp,0,sizeof(dp));
    memset(col,0,sizeof(col));
    tot=0;
}

void dfs(int u,int pre){
    dp[u][col[u]]=1;
//  printf("u= %d %d %d col=%d\n",u,dp[u][0],dp[u][1],col[u]);
    for(int i=head[u];i!=-1;i=edge[i].next){
        int v=edge[i].to;
        if(v==pre) continue;
        dfs(v,u);
        dp[u][1]=( (dp[u][1] *dp[v][0])%mod + (dp[u][0]*dp[v][1])%mod + (dp[u][1]*dp[v][1])%mod )%mod;
        dp[u][0]=( (dp[u][0] *dp[v][0])%mod + (dp[u][0]*dp[v][1])%mod )%mod;   // 改成这个莫名过:dp[v][1]
//      printf("u= %d %d %d   v=%d %d %d\n",u,dp[u][0],dp[u][1],v,dp[v][0],dp[v][1]);
    }
}
int main() {
    //freopen("1.txt","r",stdin);
    int n;
    init();
    scanf("%d",&n);
    int x;
    for(int i=1;i<n;i++){
        scanf("%d",&x);
        addedge(i,x);
        addedge(x,i);
    }
    for(int i=0;i<n;i++){
        scanf("%d",&col[i]);
    }
    dfs(0,-1);
    printf("%I64d\n",dp[0][1]);

    return 0;
}

codeforces 219D D. Choosing Capital for Treeland(树形dp)
http://codeforces.com/contest/219/problem/D

n个点 n-1条边 道路是单向路

选一个点,能从这个点到达其他所有点。
如果选择 一点为首都,那么所有路都要变为有向。因此有些路需要被反转
问选哪个(些)点,被反转的路最少。

输入反转路的数量
和选择哪些点。

我们考虑 u->v

假设 dp[v]:从v 开始 往下的子树中的最小反转次数,那么这个很好解决

那么:我们必须知道 v 往上的最小反转次数
而这个似乎是不能找出来的, 能想到的方法就是将这个点转换为根

那么如果我们 知道dp[root] 代表root 到所有点的最小反转次数
那对于root(u)->v
up[v] = dp[u] + u-v是否需要反转.
这是成立的,虽然看起来似乎不可思议。

明显dp[root]就是到所有点的最小次数
我们可以理解为 我们如果以v 为根,那么我们只需要把 u->v 改为v->u + v->v的子树
其他地方就按原来的更改即可。
如果uv的方向是正向的, dp[u]没有更改,但是
如果方向为 v-u , 那么dp[u]中肯定改成u-v 我们不需要更改这条边。


const ll mod=1e9+7;
#define N 200005
vector<int>G[N];
int dp[N];  // i以及 子树  i都可达的最小的反转次数
map<int,map<int,int> >mp;
void add(int u,int v){
    G[u].push_back(v);
    G[v].push_back(u);
}
void dfs(int u,int pre){
    dp[u]=0;
    for(int i=0;i<G[u].size();i++){
        int to=G[u][i];
        if(to==pre) continue;
        int flag=1;
        if(mp[u][to]==1)
            flag=0;
        dfs(to,u);
        dp[u]+=dp[to]+flag;
//      printf("u= %d %d %d\n",u,to,dp[u]);
    }
}
void dfs2(int u,int pre){
    for(int i=0;i<G[u].size();i++){
        int to=G[u][i];
        if(to==pre) continue;
        int flag=1;
        if(mp[u][to]==0)    //
            flag=-1;
        dp[to] =dp[u] + flag;   // ->dp:i 可达整棵树
        dfs2(to,u);
//      printf("u= %d %d %d %d\n",u,to,dp[u],dp[to]);
    }
}
int main() {
    //freopen("1.txt","r",stdin);
    int n;
    scanf("%d",&n);
    int u,v;
    mp.clear();
    for(int i=1;i<=n;i++)
        G[i].clear();
    for(int i=1;i<n;i++){
        scanf("%d %d",&u,&v);
        add(u,v);
        mp[u][v]=1;
    }
    dfs(1,0);
    dfs2(1,0);

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

状压dp:

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值