题目来自NOIP2007TG3
如果在考场上我现在已经歇菜了吧
今天一整天的时间全部投在这道题上,收获不小。
先上题目
【问题描述】
帅帅经常跟同学玩一个矩阵取数游戏:对于一个给定的n*m 的矩阵,矩阵中的每个元素aij均
为非负整数。游戏规则如下:
1. 每次取数时须从每行各取走一个元素,共n个。m次后取完矩阵所有元素;
2. 每次取走的各个元素只能是该元素所在行的行首或行尾;
3. 每次取数都有一个得分值,为每行取数的得分之和,每行取数的得分= 被取走的元素值*2i,
其中i 表示第i 次取数(从1 开始编号);
4. 游戏结束总得分为m次取数得分之和。
帅帅想请你帮忙写一个程序,对于任意矩阵,可以求出取数后的最大得分。
第1行为两个用空格隔开的整数n和m。
第2~n+1 行为n*m矩阵,其中每行有m个用单个空格隔开的非负整数。
输出 仅包含1 行,为一个整数,即输入矩阵取数后的最大得分。
2 3
1 2 3
3 4 2
82
样例解释
第 1 次:第1 行取行首元素,第2 行取行尾元素,本次得分为1*21+2*21=6
第2 次:两行均取行首元素,本次得分为2*22+3*22=20
第3 次:得分为3*23+4*23=56。总得分为6+20+56=82
【限制】
60%的数据满足:1<=n, m<=30, 答案不超过1016
100%的数据满足:1<=n, m<=80, 0<=aij<=1000
这样我们把空间从n2降到了n。
接下来开始考虑思路。不难发现,取走一部分数字后,剩下的数字总是形成一个区间。由于取走的数的个数已知,剩下的数字按顺序的权值也就知道了,由此得出,这是一个区间DP,和石子合并类似。它满足最优子结构,也满足无后效性。
用f[i][j]表示区间[i, j]的最优解,有两种解法
f[i][j] = max(a[i] + 2 * f[i+1][j], a[j] + 2 * f[i][j-1]);//直接计算数的权值 f[i][j] = max(a[i]*2^(m-j+i) + f[i+1][j], a[j]*2^(m-j+i) + f[i][j-1])//每次翻倍
第二种,也是更加方便的一种,只需要每次将小区间乘二即可,应用了乘法分配律的原理。进行完整个区间后,区间长度小乘的次数就多,最后的效果也是二的幂次方。应该也不难理解吧。
时间复杂度:O(n*m^2)
这样,我们这道题的框架就出来了,程序如下:
//codevs1166 矩阵取数游戏 区间DP+高精 //copyright by ametake #include<cstdio> #include<cstring> #include<algorithm> using namespace std; const int maxn=80+5; const int maxl=32; int n,m; int a[maxn],f[maxn][maxn],aa[maxn],bb[maxn],ans[maxn]; int main() { int ans=0; scanf("%d%d",&n,&m);//n行m列 for (int i=1;i<=n;i++) { for (int j=1;j<=m;j++) scanf("%d",&a[j]); for (int j=1;j<=m;j++) f[j][j]=a[j]; for (int j=1;j<=m-1;j++)//区间长度 { for (int k=1;k<=m-j;k++)//起点 { int l=k+j; f[k][l] = max(a[k] + 2 * f[k+1][l], a[l] + 2 * f[k][l-1]); } } ans+=2*f[1][m]; } printf("%d\n",ans); return 0; }
我们假设一种极端情况,当矩阵为80*80,且每项为1000时,结果最大,为193428131138340667952988000000。
不用数啦,这个数字是30位的。
既然这样显然我们要用高精度了。我们没有JAVA神奇的大整数,但我们也不必像pascal那样酷比手写,我们有C++的重载运算符(pas也有但是你敢用吗= =被限制的死死的一不小心就报错而且并没有什么用)
第一次手写如此大规模的高精重载,我耗费了整整一上午的时间。
因为没有任何经验,我找到了CSDN IcEnternal的代码。在这里引用一下
#include <cstdio> #include <cstring> #include <algorithm> using namespace std; //代码非原创,来源CSDN用户IcEnternal 源地址http://blog.csdn.net/devillaw_zhc/article/details/7776578 const int power = 1; //每次运算的位数为10的power次方,在这里定义为了方便程序实现 const int base = 10; //10的power次方。 //要压位的时候,只需改power 和 base即可,如压万位高精,那么power = 4, base = 10000 const int MAXL = 1001; //数组的长度。 char a[MAXL], b[MAXL]; struct num { int a[MAXL]; num() { memset(a, 0, sizeof(a)); } //初始化 num(char *s) //将一个字符串初始化为高精度数 { memset(a, 0, sizeof(a)); int len = strlen(s); a[0] = (len+power-1) / power; //数的长度 for (int i=0, t=0, w; i < len ;w *= 10, ++i) { if (i % power == 0) { w = 1, ++t; } a[t] += w * (s[i]-'0'); } //初始化数组,这里自己模拟一下,应该很容易懂的~ } void add(int k) { if (k || a[0]) a[ ++a[0] ] = k; } //在末尾添加一个数,除法的时候要用到 void re() { reverse(a+1, a+a[0]+1); } //把数反过来,除法的时候要用到 void print() //打印此高精度数 { printf("%d", a[ a[0] ]); //先打印最高位,为了压位 或者 该高精度数为0 考虑 for (int i = a[0]-1;i > 0;--i) printf("%0*d", power, a[i]); //这里"%0*d", power的意思是,必须输出power位,不够则前面用0补足 printf("\n"); } } p,q,ans; bool operator < (const num &p, const num &q) //判断小于关系,除法的时候有用 { if (p.a[0] < q.a[0]) return true; if (p.a[0] > q.a[0]) return false; for (int i = p.a[0];i > 0;--i) { if (p.a[i] != q.a[i]) return p.a[i] < q.a[i]; } return false; } num operator + (const num &p, const num &q) //加法,不用多说了吧,模拟一遍,很容易懂 { num c; c.a[0] = max(p.a[0], q.a[0]); for (int i = 1;i <= c.a[0];++i) { c.a[i] += p.a[i] + q.a[i]; c.a[i+1] += c.a[i] / base; c.a[i] %= base; } if (c.a[ c.a[0]+1 ]) ++c.a[0]; return c; } num operator - (const num &p, const num &q) //减法,也不用多说,模拟一遍,很容易懂 { num c = p; for (int i = 1;i <= c.a[0];++i) { c.a[i] -= q.a[i]; if (c.a[i] < 0) { c.a[i] += base; --c.a[i+1]; } } while (c.a[0] > 0 && !c.a[ c.a[0] ]) --c.a[0]; //我的习惯是如果该数为0,那么他的长度也是0,方便比较大小和在末尾添加数时的判断。 return c; } num operator * (const num &p, const num &q) //乘法,还是模拟一遍。。其实高精度就是模拟人工四则运算! { num c; c.a[0] = p.a[0]+q.a[0]-1; for (int i = 1;i <= p.a[0];++i) for (int j = 1;j <= q.a[0];++j) { c.a[i+j-1] += p.a[i]*q.a[j]; c.a[i+j] += c.a[i+j-1] / base; c.a[i+j-1] %= base; } if (c.a[ c.a[0]+1 ]) ++c.a[0]; return c; } num operator / (const num &p, const num &q) //除法,这里我稍微讲解一下 { num x, y; for (int i = p.a[0];i >= 1;--i) //从最高位开始取数 { y.add(p.a[i]); //把数添到末尾(最低位),这时候是高位在前,低位在后 y.re(); //把数反过来,变为统一的存储方式:低位在前,高位在后 while ( !(y < q) ) //大于等于除数的时候,如果小于的话,其实答案上的该位就是初始的“0” y = y - q, ++x.a[i]; //看能减几个除数,减几次,答案上该位就加几次。 y.re(); //将数反过来,为下一次添数做准备 } x.a[0] = p.a[0]; while (x.a[0] > 0 && !x.a[x.a[0]]) --x.a[0]; return x; } int main() { scanf("%s", a); scanf("%s", b); reverse(a, a+strlen(a)); reverse(b, b+strlen(b)); p = num(a), q = num(b); ans = p + q; ans.print(); ans = p - q; ans.print(); ans = p * q; ans.print(); ans = p / q; ans.print(); }
我模仿着在结构体里写下了这样的代码(上述代码中并没有):
num operator = (int b) //将一个常数赋值给高精结构体 { num c; c.a[0]=0; while (b) { c.a[0]++; c.a[c.a[0]]=b%base; b/=base; } return c; }
num operator + (int b) //高精结构体加常数 { num c; c.a[0]=a[0]; c.a[1]+=b; int i=1; while (c.a[i]>=base) { c.a[i+1]+=c.a[i]/base; c.a[i]%=base; i++; } if (c.a[c.a[0]+1])++c.a[0]; return c; }
主函数输出:
int main() { scanf("%s", a); scanf("%s", b); reverse(a, a+strlen(a)); reverse(b, b+strlen(b)); p = num(a), q = num(b); ans = p + q; ans.print(); ans=12345; ans.print(); ans = ans + 50; ans.print(); while (1); return 0; }
于是我翻出从前wfwbz神犇写的高精模板,参照那个把前面赋值常数改成了下面这样,尽管并不理解为什么,结果成功了
num operator = (int b) { a[0]=0; while (b) { a[0]++; a[a[0]]=b%base; b/=base; } return *this; }
怎么调也没办法,里奥神犇表示爱莫能助。这时候我才深深体会到朋友和前辈的重要性。这道题目的解决,尤其要感谢广饶一中YQL同学和去年一等的那位神犇,清华的HYM神犇以及即将清华的Ag爷WYW神犇,耐心给我解释了两个小时···尤其是HYM前辈,小辈各种无知仍然不要其烦,深受感动。原本发出求助后以为没人注意我,结果一会儿大家都来回答,真的很感动= =
碎碎念到此为止,总之听了各位前辈的讲解总结出以下几点:
1.赋值号和加号作用原理不一样,分开讨论,不能一概而论。
2.对于赋值号,上述做法中的第一种错在不能重新定义一个num c,因为这个变量是局部变量,在整个函数(重载相当于跑一个函数)进行完的时候自动销毁收回内存。那么return c不起作用吗?不起作用。赋值号有自己的返回值。如果这里我们写(ans=123).print(),输出是123,因为这里输出的是返回值,赋值号return c,c=123,就输出123。但是return的这个c并没有赋值给等号左边,只返回一个值但没有效果。
那么怎么办呢?在operator中,对于赋值号左边有一个只在函数内壁生效的名称,那就是this。
this是一个指针,指向赋值号左边的元素。在这里,它指向被复制的num结构体。
也就是说,对*this操作就是在对等号左边操作。比如,我们想使ans.a[0]=b,就可以写(*this).a[0]=b,或者this->a[0]=b;更推荐后面一种写法。
最后,我们返回*this,作为赋值号的返回值。尽管并不太懂得赋值号返回值有什么用,但这样就能保证赋值和输出都正确。
更改后就是上面的第二种写法
3.对于加号,结构体加结构体的话,可以开一个临时结构体c,由于最后起作用的是赋值号,这个c会被赋值给左边的ans。但是,如果写结构体加常数就不可以。
为什么呢?HYM神犇是这样说的
这两个确实是不一样的,你先记忆一下好了。大学会学的。
总之大概是我的智商难以承受的东西吧···ORZ神犇的世界
总之更改之后,应当是这样子:
num operator + (const int &b) { this->a[1]+=b; int i=1; while (this->a[i]>=base) { this->a[i+1]+=this->a[i]/base; this->a[i]%=base; i++; } if (this->a[this->a[0]+1])this->a[0]; return *this; }
num operator + (const int &b)//this->都可以省略 { a[1]+=b; int i=1; while (a[i]>=base) { a[i+1]+=a[i]/base; a[i]%=base; i++; } if (a[a[0]+1])a[0]++; return *this; }
这里还要补充一个小问题,由WYW神犇友情解答:
bool operator < (const num &b)const
最后的最后,我之所以调了一下午代码,仅仅是因为一个低级错误:
c.a[i]+=a[i]+b.a[i];
事实证明,千里之堤,溃于蚁穴。细节决定成败TUT
好了,讲完了,放出代码君:
——西塞山前白鹭飞,桃花流水鳜鱼肥
版权声明:转载请注明出处 [ametake版权所有]http://blog.csdn.net/ametake欢迎来看
【日常学习】【区间DP+高精】codevs1166 矩阵取数游戏题解
原文地址:http://blog.csdn.net/ametake/article/details/47664889