poj 2414 Phylogenetic Trees Inherited 完全二叉树 状态压缩位运算模拟集合操作 动态规划
题目题意异常恶心,难以理解......
呵呵.....不过题意是 Lyush大神 花费一小时看明白后透露给笔者的, 到现在依然YM此大婶当中......
先说说题目大意:
一颗完全二叉树,有 N ( n <= 1024,且必定为2的整数幂,意味着是一颗完全二叉树 ) 个叶子节点,
每一个节点都含有一个长度为 L L ( L <= 1000 ) 的串 ( 串仅由大写字母构成), 现仅仅知道N个叶子节点串的组成.
其他节点串不知道.
但我们知道,直接父节点相同的两个子节点,其对应位置不同则花费为1. 整棵树花费最小.
让你求,整棵树的最小花费, 以及根节点的可能串元素组成.若有多种情况则随意输出一种即可. ( Special Judge )
解题思路:
第一点我们知道每一个串中不同位置间无约束条件. 所以我们可以分别处理.
定义函数
T( rt ) 表示以 rt 为根节点的二叉树最小花费值
A( rt ) 表示以 rt 为根节点的二叉树最小花费值时, 可选取的字符的集合
那么,若对于 有相同直接父节点 rt 的节点 lch, rch 已得出 T( lch ), A( lch ) 与 T( rch ), A( rch)
那么对于 其父节点 rt 来看, 若 T( rt ) 要取得最优解, 则
or //这里的并与交,指代集合的操作
对于这个结论,我们可以通过归纳法证明:
有三种情况:
一, 当 , 则 lch,rch位置字符与rt仅有一个不同 花费 cost = 1
二, 当 , 则 lch,rch位置字符与rt都相同 花费 cost = 0
三, 当, 则lch,rch位置字符与rt两两间都不同,花费 cost = 2
所以我们可以得出结论.
当且仅当, , 此时其父节点rt, , 并且此时花费 cost = 0
否则, 此时 , 其父节点rt, , 并且此时 子节点 lch, rch 中必有一个与根节点不同,所以花费cost = 1
换句话说,我们每次合并集合,则花费cost必定+1
关于编码技巧问题, 这里我们除了将不同位置分开处理之外, 还可以观察到, 串元素仅有大写字母A-Z组成,共26个.属于int范围内.
因为可选项最多26个,若我们用数组来模拟集合,则势必多一层26的循环,时间复杂度增大不少, 我们可以使用 位运算,状态压缩到一个 int 值当中,
这样集合的 交与并 操作则可以使用 位运算的 &与| 来替代.
AC代码:
#include<stdio.h> #include<string.h> #include<stdlib.h> char seq[1024][1024]; int T[2048], n, m; int main() { while( scanf("%d%d", &n,&m) , n+m ) { for(int i = 0; i < n; i++) scanf("%s", seq[i]); int cost = 0; for(int p = 0; p < m; p++) { for(int x = 0; x < n; x++) T[x+n] = 1<<(seq[x][p]-'A'); for(int rt = n-1; rt >= 1; rt-- ) { int lch = rt<<1, rch = rt<<1|1; T[rt] = T[lch]&T[rch]; if( T[rt] == 0 ) { T[rt] = T[lch] | T[rch]; cost++; } } char ch = 'A'; while( (T[1]&1) == 0 ) { ch++; T[1] >>= 1; } printf("%c", ch ); } printf(" %d\n", cost ); } return 0; }
再贴个搓代码, 前面直接模拟的写法,外加数组模拟集合操作,时间+空间一起T..虽然不好,总也是自己想了好久的......
#include<stdio.h> #include<string.h> #include<stdlib.h> #include<math.h> const int inf = 0x3fffffff; typedef long long LL; #define MIN(a,b) (a)<(b)?(a):(b) struct Node { bool num[1001][27]; // 27001 Byte char s[1001]; // 1001 Byte }node[1100<<1]; // 2200 5*10^7 B int n, m, mf; LL ans; void print_n( int rt ) { printf("rt = %d\n", rt ); for(int p = 0; p < m; p++) { printf("p=%d : ", p); for(int op = 0; op < 26; op++) { printf("%d ", node[rt].num[p][op] ); } printf("\n\n"); } } void print_s( int rt ) { printf("rt = %d: ",rt); for(int p = 0; p < m; p++) printf("%d ", node[rt].s[p] ); puts(""); } void init() { //初始化最后一层可选项 for(int i = n; i < n+n; i++ ) { memset( node[i].num, 0, sizeof( node[i].num ) ); for(int p = 0; p < m; p++) node[i].num[p][ node[i].s[p]-'A' ] = true; } // 从下层 log2(n)+1 往上更新固定值,或单一固定值 // 对于无法确定的则保存下当前可选择项 // 预处理最后一层与上一层的关系 for(int f = mf; f > 1; f-- ) { //第f层有 (2^(f-1)) 个节点, 且从 (2^(f-1))开始 int x = 1<<(f-1); for(int lch = x; lch < x+x; lch += 2 ) { int rch = lch+1, fa = lch/2; //printf("fa = %d, lch = %d, rch = %d\n", fa, lch, rch ); //初始化父节点fa,可选项 memset( node[fa].num, 0, sizeof(node[fa].num) ); node[fa].s[m] = '\0'; //对每个构造串手动添加字符串结尾标志 // 处理每个位置p, 位置总长m for(int p = 0; p < m; p++) { //printf("l.s=%d,r.s=%d\n",node[lch].s[p],node[rch].s[p] ); if( (node[lch].s[p] == 0) || (node[rch].s[p] == 0) ) { node[fa].s[p] = 0; } else{ //左右子节点 串p 位置都为固定值 if( node[lch].s[p] != node[rch].s[p] ) node[fa].s[p] = 0; else node[fa].s[p] = node[lch].s[p]; } for(int ch = 0; ch < 26; ch++ ) node[fa].num[p][ch] = (node[lch].num[p][ch] || node[rch].num[p][ch]); } // print_s(fa); } } } // 以rt为根节点的树, 其父节点选择fa_op的最小花费 int dfs( int p, int rt, int fa_op ) { int floor = (int)(log2(1.*rt))+1; //目前层数 int fa = rt/2; bool flag = false; // 标记值,判定节点rt是否需要选择,默认为不需要 // 若节点rt, p位置字符固定,则该节点下面的子树该位置都相同,无需继续选择 // 因为mf层,都没确定值,则当递归到mf层时,必定开始回溯. if( node[rt].s[p] != 0 ) { if( node[rt].s[p]-'A' == fa_op ) return 0; else return 1; } else { //若不确定,则分为两种情形: // 一,当节点rt,包含其父节点fa_op选项时选择,rt同时选择该选项 // 二,当节点rt,不包含其父节点fa_op选项时,rt开始枚举选择最优选项 .并且此时导致 花费+1 LL c = 0; if( node[rt].num[p][ fa_op ] == true ) { c += dfs( p, rt<<1, fa_op ); c += dfs( p, rt<<1|1, fa_op); return c; } else { // 此时应在所有可选项中取一个最小值 c = inf; for(int op = 0; op < 26; op++) { LL tmp = 0; if( node[rt].num[p][op] == true ) { //若当前op可选 tmp += dfs( p, rt<<1, op ); tmp += dfs( p, rt<<1|1, op ); c = MIN( c, tmp ); } } return c+1; //因为此位置与父节点不想符合,造成花费额外+1 } } } void solve(){ ans = 0; // 总花费 // 确定位置P 最小花费cost,以及最佳选项 for(int p = 0; p < m; p++) { // 若p位置字符不确定,则进行搜索 // printf("node[1].s[%d] = %d\n",p, node[1].s[p] ); // printf("%d ", node[1].s[p] ); if( node[1].s[p] == 0 ) { LL cost = inf; int ch = -1; for(int op = 0; op < 26; op++) { //printf(" node[%d].num[%d][%d] = %d\n", 1, p, op, node[1].num[p][op] ); if( node[1].num[p][op] ) //当前字符可选 { LL c = 0; // 初始化 当选择OP选项时,最小花费 //假定 父串p位置取op选项,然后递归处理下一层 c += dfs( p, 2, op ); //位置p, 子树根2, 父节点选项op c += dfs( p, 3, op ); // printf("c = %lld\n", c ); if( cost > c ) //取最小花费c,所在选项op { cost = c; ch = op; } } } //确定p位置字符为 ch, 最小花费为 cost; ans += cost; node[1].s[p] = ch+'A'; // printf("ch = %d, cost = %lld\n", ch, cost ); } } //输出 printf("%s %lld\n", node[1].s, ans ); } int main() { freopen("8.in","r", stdin); freopen("8.out","w",stdout); while( scanf("%d%d", &n,&m) , n+m ) { mf = (int)(log2(1.*n))+1; //printf("mf = %d\n", mf ); for(int i = n; i < n+n; i++) scanf("%s", node[i].s ); // 由下而上更新 可确定项,以及生成可选项 init(); // 由上而下搜索位置p 可选项取最小值 solve(); } return 0; }