HDU 5877 :http://acm.hdu.edu.cn/showproblem.php?pid=5877
题意 : 给N个点构成一颗树,再给一个K 。 寻找有几对 “虚弱的点对” 。 点对要求是,祖先节点 乘以 孩子节点的值小于等于K ,则这个祖先孩子点对为虚弱点对。。
啊,说实话,我是一点都没看出来这题和树状数组(区间求和)有个鬼联系。。。。 不看网上的题解,我表示想不出。。。
很可惜的是,即使我看了网上的题解,我也没有理解他们的思维所在。。。硬是墨墨叽叽啃了半天 DFS的代码,才稍微看懂了 如何用树状数组去联系这道题。。。
据说是大连区域赛网赛的最后一题。。。
还有两点,其一是,自己依然没有将离散化使用的融会贯通。比起直接使用map,我更倾向于自己写循环映射,但是不方便,多数情况下还是用map来进行离散操作来的方便;然而,刚刚发现离散化还有模板的,更加高贵优雅了。。。
其二是细节,此题的给出的数字的可以为0的情况,要细细思考(也许不用,仅仅是我太蠢),交了好多发总是WA。敲了2年多了代码,总是还有这个字母那个数组大小这个变量名混了的低级错误,导致自己找BUG找一百年啊一百年。。。水平还是需要提升的,不能太浮躁。
接下来讲一下此题的思路。大佬们做题目都是脑旁一个灯泡一亮,就悟道了;于是我这种咸鱼,看别人的博客,感受着别人“呐,就是这样就好了就A掉了”的情绪之时,我不但没看懂他们的代码思路,一脸懵逼之余,道心受到了巨大的创伤。。。。现在我讲题解,一定要讲的尽量健康快乐。。。。/(ㄒoㄒ)/~~
思路: 我们用数组A来保存每一个节点的值。
此题要求的“虚弱的点对”数,所需要满足的条件 必须要是 祖先 和 孩子 之间。 判断条件是否成立,也就是判断 “ A祖先 * A孩子 <= K ” 是否成立。
我们对这个条件移项,就可以得到 “ A祖先 <= K/(A孩子) ” 或者是 “ A孩子 <= K/(A祖先) ”
根据这个移项以后的等式,我们可以将问题转化为 ,
枚举每一个节点v ,我们要找出 小于等于 “ K/Av][” ,也就是小于等于这个商 的所有祖先节点的个数。 然后累加在一起。
等价的是 枚举每一个节点v ,我们要找出 小于等于 “ K/A[v]” ,也就是小于等于这个商 的所有孩子节点的个数。 然后累加在一起。
如果我们针对每个节点,来枚举孩子的话,会相对有些麻烦;因为每个节点的孩子,我们都要重新搜一遍才晓得; 如果我们针对每一个节点,来枚举他们的祖先节点,就会相对方便一些, 这是由于我们从根节点开始往下DFS的过程中,按照DFS的基本特性,是顺着一条树链向下搜的,结合回溯法以后,我们便可以实现“边DFS,边计算”的思想。
那么,树状数组在这里怎么体现的呢? ——对于一个点v,如何查找 满足条件的祖先节点的个数是多少个呢? 标记作用的树状数组经典实用方法。
满足条件 ——也就是 祖先点值 <= K/ A[V] 。 我们将所有节点的值A[i]以及每个节点对应的商 ,都放入到树状数组中。那么我们可以在DFS过程中,每搜过一个节点,就将此节点的值 在树状数组中标记为1,含义为“此点出现过了”。(add操作)而统计祖先节点的过程, 就是 在树状数组中 ,将小于等 当前节点 的商 的所有值的标记累加,进行树状数组的求和操作。(cal_sum操作) 在一条树链的DFS搜索结束以后,还要回溯,消除掉这个标记,因为对别的树链而言,这条树链就不是它那条树链的祖先了,就是使用add操作加上-1即可。
离散化操作。错了很多遍,很多问题都是出现在离散化上面。
为什么要离散,因为树状数组的大小太大是会爆的。我们只能开500000的大小,而A[i]的范围却是999999999,所以要把这些A[i]离散掉利于解题。
注意的是:这道题是 不仅仅要离散所有A[i],还要离散 K/A[i] ,并且是同时离散,离散在一个数组里面。 为的是实现 add(A[i]) ,而 cal_sum( K/A[i] )。 所以树状数组的大小是2*n 而不是n 。要注意越界问题。
A[i] 可以等于0 的细节问题。
当A[i}等于0的时候,是没办法计算K/A[i] 的,会出现错误,所以要注意一下。那如何处置呢。我们前面讨论过,点值是为了更新,商值是为了求和。而但A[i]等于0的时候,其所有子节点都是满足条件的。所以我们不能将其离散为-1(为什么是-1,因为我就是-1然后WA了。。),应当映射为无穷大(取9个9就行了哈)。
这是我找了万年BUG以后终于A掉的代码,但是后面我会放上经过我的修改以及排版之后,别的大佬的代码,他们的思路更加清晰直观,最好看我的博客思路,代码还是看第二种的好一些~~
我的垃圾代码: 用map来离散。这里的pos数组在DFS中没什么用,因为是为了用来方便map离散的。
#include"bits/stdc++.h"
using namespace std;
#define inf 100009
#define INF 999999999
#define loop(x,y,z) for(x=y;xedge[inf]; //边
int node[inf]; //点值
ll pos[inf*2]; //每个点的 k/node[i]
mapm;
int c[2*inf]; //树状数组
ll ans; //最终结果
int book[inf]; //标记数组,为了找出根节点
void init()
{
int i;
loop(i,1,n+1)edge[i].clear();
m.clear();
memset(c,0,sizeof c);
memset(book,0,sizeof book);
ans=0;
}
int lowbit(int i)
{
return i&-i;
}
int cal_sum(int i)
{
int sum=0;
while(i>0)
{
sum+=c[i];
i-=lowbit(i);
}
return sum;
}
void add(int i,int t)
{
while(i<=2*n)
{
c[i]+=t;
i+=lowbit(i);
}
}
void dfs(int u)
{
ll t=node[u]?m[k/node[u]]:2*inf-10; //这个值所对应的映射
ans+=cal_sum(t);
int len=edge[u].size(),i;//printf("%d\n",m[pos[u]]);
add(m[node[u]],1);
loop(i,0,len)
{
int v=edge[u][i];
dfs(v);
}
add(m[node[u]],-1);
}
int main()
{
int i,j,T,root;
scanf("%d",&T);
while(T--)
{
init();
scanf("%d%lld",&n,&k);
loop(i,1,n+1)
{
scanf("%d",&node[i]); //存放每个点的值
if(node[i])
pos[i]=k/node[i]; //每个点的k/node[i]
else pos[i]=-1;
pos[i+n]=node[i];
}
//开始映射
sort(pos+1,pos+1+n+n);
j=0;
loop(i,1,2*n+1)
{
if(pos[i]!=pos[i-1])j++;
m[pos[i]]=j;
}
loop(i,1,n)
{
int u,v;
scanf("%d%d",&u,&v);
edge[u].push_back(v);
book[v]=1;
}
loop(i,1,n+1)if(!book[i]){root=i;break;} //找根
dfs(root);
printf("%lld\n",ans);
}
return 0;
}
另一种高级代码: 用模板来离散,离散以后的离散表就是pos数组,pos数组的前n个为节点值的离散,后n个为 商值 的离散哦。所以DFS那边一会是 u+n 一会是 u,要理解。
#include
#include
#include
#include
#include
#include
#define ll __int64
using namespace std;
#define inf 100009
#define INF 999999999
#define loop(x,y,z) for(x=y;xedge[inf];
ll k,ans;
ll pos[inf*2],li[inf*2];
void init()
{
//m.clear();
int i;
loop(i,1,n+1)edge[i].clear();
memset(c,0,sizeof c);
memset(book,0,sizeof book);
ans=0;
}
int lowbit(int i)
{
return i&-i;
}
int cal_sum(int i)
{
int sum=0;
while(i>0){
sum+=c[i];
i-=lowbit(i);
}
return sum;
}
void add(int i,int t)
{
while(i<=2*n){
c[i]+=t;
i+=lowbit(i);
}
}
void dfs(int u){
ans+=cal_sum(pos[u+n]);//在它之前进入的都为它的祖先
add(pos[u], 1);
for(int i=0; i