【前方高能】!后效性!

今天校内测

由于忽视了后效性的问题,很happy地爆炸了

然后就华华丽丽地炸掉了树形dp。。AK→220

然后悲剧的想到因为没有消掉后效性而炸掉的dp题好像不是第一个了QAQ

所以这波就讲关于dp中消除后效性的问题

dp中有一个很重要的问题就要保证无后效性,在状态转移的过程中非常关键

简单说,后效性就是之后要求的决策不会对当前要求决策产生干扰

所以如果没有处理好后效性的问题,dp很有可能就是白写了

比较常见的就是迭代之类的问题

,,今天就是在迭代的时候崩掉的

就以这题为例↓

3.Nearby Cows
问题描述:
给出一棵 N 个点的树,每个点上都有牛,问每个点 K 步范围内有多少牛。牛的数量包括这个点本身。
输入格式:
第一行为 N 和 K。
以下 2..N 行,每行两个整数,表示两个点相连。点的范围在 1..N。
以下 N 行,每行一个整数, 表示每个点上牛的数量。按点的编号的顺序给出。数量范围在 0..1000。
输出格式:
按点的编号从小到大输出牛的数量。
输入样例:
6 2
5 1
3 6
2 4
2 1
3 2
1
2
3
4
5
6
输出样例:
15
21
16
10
8
11
数据范围:
1 <= N <= 100,000
1 <= K <= 20

其实是道很水的树形dp

状态转移方程很好推

把树给定下来之后,先从叶子节点往根节点上推一波,f[i][j]表示以i为根节点的子树内从i开始恰好走j步的节点的权重,假定从i到i要走1步

f[i][j]=∑f[i的儿子节点][j-1]

刷完之后

迭代一遍把f[i][j]的含义变成以i为根节点,j步范围内的权重和,就是前缀和的原理

for (int i=1;i<=n;i++)
 for (int j=2;j<=k;j++)
  f[i][j]+=f[i][j-1];

然后从根节点往叶子节点推一发,把除了子树内的其他点也迭代上

所以f[i][j]=f[i的父亲节点][j-1]-f[i][j-2]后面减去的东西是因为i的子树内的东西也被i的父节点算过一次,重复了

代码在下面↓

 var link,fa,que,a:array[0..100005]of longint;
     son,next:array[0..200005]of longint;
     vis:array[0..100005]of boolean;
     f:array[0..100005,0..25]of longint;
     n,k,tot,head,tail:longint;
 procedure add(x,y:longint);
  begin
   inc(tot);son[tot]:=y;next[tot]:=link[x];link[x]:=tot;
  end;
 procedure init;
  var i,j,x,y:longint;
   begin
    assign(input,‘nearcows.in‘);reset(input);
    assign(output,‘nearcows.out‘);rewrite(output);
    fillchar(link,sizeof(link),0);
    readln(n,k);tot:=0;inc(k);
    for i:=1 to n-1 do
     begin
      readln(x,y);
      add(x,y);add(y,x);
     end;
    for i:=1 to n do readln(a[i]);
   end;
 procedure main;
  var i,j,x,t:longint;
   begin
    fillchar(vis,sizeof(vis),0);
    fillchar(que,sizeof(que),0);
    head:=0;tail:=1;que[1]:=1;vis[1]:=true;
    fillchar(fa,sizeof(fa),0);
    while head<>tail do
     begin
      inc(head);
      x:=que[head];
      j:=link[x];
      while j<>0 do
       begin
        if not vis[son[j]] then begin
                                 inc(tail);
                                 que[tail]:=son[j];
                                 vis[son[j]]:=true;
                                 fa[son[j]]:=x;
                                end;
        j:=next[j];
       end;
     end;
   fillchar(f,sizeof(f),0);
   for i:=n downto 1 do
    begin
     x:=que[i];
     f[x,1]:=a[x];
     j:=link[x];
     while j<>0 do
      begin
       if fa[x]<>son[j] then
        begin
         for t:=1 to k-1 do
          f[x,t+1]:=f[x,t+1]+f[son[j],t];
        end;
       j:=next[j];
      end;
    end;
   for i:=1 to n do for j:=2 to k do f[i,j]:=f[i,j-1]+f[i,j];
   for i:=1 to n do
    begin
     x:=que[i];
     if fa[x]=0 then continue;
     for t:=1 to k-1 do
      f[x,t+1]:=f[x,t+1]-f[x,t-1]+f[fa[x],t];
    end;
   end;
 procedure print;
  var i:longint;
   begin
    for i:=1 to n do writeln(f[i,k]);
    close(input);close(output);
   end;
 begin
  init;
  main;
  print;
 end.

从头看到尾,感觉并没有什么问题==

====== ======

事实上,这个程序是WA的

问题在这里↓

把循环改成for t:=k-1 downto 1 do 就没问题了,原因就是后效性

因为每次求的时候要调用f[x,t-1],且因为要减去的f[x,t+1]和f[fa[x],t]的重复部分是以x为根的子树内的东西,而每次更新之后f[x,t+1]都不再只是子树内的权重和,而是所有从x为起点t+1步内的权重和,所以要保证每次掉的f[x,t-1]还没被更新,t只能反着枚举,即for t:=k-1 downto 1 do

下面的是AC代码↓

 var link,fa,que,a:array[0..100005]of longint;
     son,next:array[0..200005]of longint;
     vis:array[0..100005]of boolean;
     f:array[0..100005,0..25]of longint;
     n,k,tot,head,tail:longint;
 procedure add(x,y:longint);
  begin
   inc(tot);son[tot]:=y;next[tot]:=link[x];link[x]:=tot;
  end;
 procedure init;
  var i,j,x,y:longint;
   begin
    assign(input,‘nearcows.in‘);reset(input);
    assign(output,‘nearcows.out‘);rewrite(output);
    fillchar(link,sizeof(link),0);
    readln(n,k);tot:=0;inc(k);
    for i:=1 to n-1 do
     begin
      readln(x,y);
      add(x,y);add(y,x);
     end;
    for i:=1 to n do readln(a[i]);
   end;
 procedure main;
  var i,j,x,t:longint;
   begin
    fillchar(vis,sizeof(vis),0);
    fillchar(que,sizeof(que),0);
    head:=0;tail:=1;que[1]:=1;vis[1]:=true;
    fillchar(fa,sizeof(fa),0);
    while head<>tail do
     begin
      inc(head);
      x:=que[head];
      j:=link[x];
      while j<>0 do
       begin
        if not vis[son[j]] then begin
                                 inc(tail);
                                 que[tail]:=son[j];
                                 vis[son[j]]:=true;
                                 fa[son[j]]:=x;
                                end;
        j:=next[j];
       end;
     end;
   fillchar(f,sizeof(f),0);
   for i:=n downto 1 do
    begin
     x:=que[i];
     f[x,1]:=a[x];
     j:=link[x];
     while j<>0 do
      begin
       if fa[x]<>son[j] then
        begin
         for t:=1 to k-1 do
          f[x,t+1]:=f[x,t+1]+f[son[j],t];
        end;
       j:=next[j];
      end;
    end;
   for i:=1 to n do for j:=2 to k do f[i,j]:=f[i,j-1]+f[i,j];
   for i:=1 to n do
    begin
     x:=que[i];
     if fa[x]=0 then continue;
     for t:=k-1 downto 1 do
      f[x,t+1]:=f[x,t+1]-f[x,t-1]+f[fa[x],t];
    end;
   end;
 procedure print;
  var i:longint;
   begin
    for i:=1 to n do writeln(f[i,k]);
    close(input);close(output);
   end;
 begin
  init;
  main;
  print;
 end.

所以在各种dp题目中,正着刷和反着刷可能会得到截然不同的结果,一定要注意

这个代码的错误其实是我美丽动人的晗翀学姐发现的,然后我才能想到后面的一连串问题=====本行字是晗翀学姐强烈要求我加上去的QAQ

【写的有漏洞的,欢迎路过大神吐槽】

2016-08-12 16:49:29

Ending.

时间: 2024-08-03 06:52:32

【前方高能】!后效性!的相关文章

【BZOJ3875】【Ahoi2014】骑士游戏 SPFA处理有后效性动规

广告: #include <stdio.h> int main() { puts("转载请注明出处[vmurder]谢谢"); puts("网址:blog.csdn.net/vmurder/article/details/44040735"); } 题解: 首先一个点可以分裂成多个新点,这样就有了图上动规的基础. 即f[i]表示i点被消灭的最小代价,它可以由分裂出的点们更新. 但是这个东西有后效性,所以我们用SPFA来处理它. spfa处理后效性动规 我

hdu2333-贪心,如何去后效性,背包太大怎么办,如何最大化最小值,从无序序列中发掘有序性质

补充一下我理解的中文题意.. 你要重新组装电脑..电脑有一些部件..你的预算有b,b(1~1e9),有n个部件..每个部件有类型和名称以及价钱和质量现在你要在不超过预算b的情况下..每个类型都买一个部件..然后最终的质量由最小的质量决定在此约束下问你在预算b之内能组装的最大的质量是多少对每个部件价钱范围1e6,质量范围1e9 =============== 由于钱和质量没有必然联系 所以我们不能直接从质量小的开始贪.. 也没法从质量大的开始贪吧..因为你还要保证你的钱要够 按质量排序则钱是无序的

(dfs痕迹清理兄弟篇)bfs作用效果的后效性

dfs通过递归将每种情景分割在不同的时空,但需要对每种情况对后续时空造成的痕迹进行清理(这是对全局变量而言的,对形式变量不需要清理(因为已经被分割在不同时空)) bfs由于不是利用递归则不能分割不同的时空,但其利用队列将不同时空下的步骤在时间上进行同步(但队列内部的并不都是同一时间的) 但必须要区分在队列内的与从队列里拿出来的时间关系,所以对于类似于取钥匙开门的操作,应该判定在钥匙位置被提出队列时才能触发开门功能,而不能认为钥匙在队列时就能开门也就是bfs的作用效果要放在位置被提出队列时展开,而

vijos[1355]车队过桥问题

描述 现有N辆车要按顺序通过一个单向的小桥,由于小桥太窄,不能有两辆车并排通过.另外,由于小桥建造的时间已经很久,只能承受有限的重量,记为Max(吨).管理员将N辆车按初始的顺序分组,每次让一个组过桥,并且只有在一个组的车辆全部过桥后,下一组车辆才能上桥.每辆车的重量和最大速度是已知的,而每组车的过桥时间由该组中速度最慢的那辆车决定.请你帮管理员编一个程序,将这N辆车分组,使得全部车辆通过小桥的时间最短. 格式 输入格式 文件的第一行有3个数字,分别为Max(吨),Len(桥的长度,单位km),

【Codevs1183】泥泞的道路

Position: http://codevs.cn/problem/1183/ List Codevs1183 泥泞的道路 List Description Input Output Sample Input Sample Output HINT Solution Code Description CS有n个小区,并且任意小区之间都有两条单向道路(a到b,b到a)相连.因为最近下了很多暴雨,很多道路都被淹了,不同的道路泥泞程度不同.小A经过对近期天气和地形的科学分析,绘出了每条道路能顺利通过的

每日一面day1

贪心算法与其弊端 贪心算法又称贪婪算法,见文思意,贪心贪心,无非就是想办法寻找最好的方法,对应到算法上即将一个问题分解成若干个小问题,每步选取当前最优解,贪心算法的弊端在于它并非对所有的问题都有效,当问题不具有最佳子结构或是贪心策略有后效性时,得出的结果可能就并不正确.相应的问题有背包问题,由此也可以引出动态规划,有兴趣的同学可自行学习

dp状态压缩

dp状态压缩 动态规划本来就很抽象,状态的设定和状态的转移都不好把握,而状态压缩的动态规划解决的就是那种状态很多,不容易用一般的方法表示的动态规划问题,这个就更加的难于把握了.难点在于以下几个方面:状态怎么压缩?压缩后怎么表示?怎么转移?是否具有最优子结构?是否满足后效性?涉及到一些位运算的操作,虽然比较抽象,但本质还是动态规划.找准动态规划几个方面的问题,深刻理解动态规划的原理,开动脑筋思考问题.这才是掌握动态规划的关键. 动态规划最关键的要处理的问题就是位运算的操作,容易出错,状态的设计也直

bzoj 2109: [Noi2010]Plane 航空管制

Description 世博期间,上海的航空客运量大大超过了平时,随之而来的航空管制也频频 发生.最近,小X就因为航空管制,连续两次在机场被延误超过了两小时.对此, 小X表示很不满意. 在这次来烟台的路上,小 X不幸又一次碰上了航空管制.于是小 X开始思考 关于航空管制的问题. 假设目前被延误航班共有 n个,编号为 1至n.机场只有一条起飞跑道,所 有的航班需按某个顺序依次起飞(称这个顺序为起飞序列).定义一个航班的起 飞序号为该航班在起飞序列中的位置,即是第几个起飞的航班. 起飞序列还存在两类

洛谷 P1412 经营与开发

P1412 经营与开发 题目描述 4X概念体系,是指在PC战略游戏中一种相当普及和成熟的系统概念,得名自4个同样以“EX”为开头的英语单词. eXplore(探索) eXpand(拓张与发展) eXploit(经营与开发) eXterminate(征服) ——维基百科 今次我们着重考虑exploit部分,并将其模型简化: 你驾驶着一台带有钻头(初始能力值w)的飞船,按既定路线依次飞过n个星球. 星球笼统的分为2类:资源型和维修型.(p为钻头当前能力值) 1.资源型:含矿物质量a[i],若选择开采