动态规划 | 较为复杂的动态规划 | poj1191,poj1054,poj3280,poj2029 ,poj2948,poj1925,poj3034 |
记录状态的动态规划 (状压DP) | poj3254,poj2411,poj1185 | |
树型动态规划 | poj2057,poj1947,poj2486,poj3140 |
较为复杂的动态规划
poj 1191
中文题,题意很简单。^_^,黑书上的原题,前几天刚做过。
先把
化简的 σ2 = 1/n*∑xi2 - ¯x;
也就是说求出∑xi2的最小值就可以了。
dp(x1, y1, x2, y2, k),表示(x1, y1), (x2, y2)所确定的区间上切k刀分成k+1块的最优值。
dp(x1, y1, x2, y2, k) = min(dp(x1, y1, i - 1, y2, k - 1) + sum(i, y1, x2, y2), dp(i, y1, x2, y2, k - 1) + sum(x1, y1, i - 1, y2),
dp(x1, y1, x2, j - 1, k - 1) + sum(x1, j, x2, y2), dp(x1, j, x2, y2, k - 1) + sum(x1, y1, x2, j - 1));
(x1 < i <= x2; y1 < j <= y2;)
然后记忆化搜索
poj 1054
题意:好多青蛙祸害庄稼,已知一个R*C的稻田。给出被青蛙踩到的庄稼的坐标,已知一只青蛙的跳跃距离是一定的,它从稻田外踩过稻田中的M个点然后又跳出稻田,求某只青蛙踩出的M最大值。(也就是说一只青蛙可以踩出一条线来。)小于三的情况直接输出0;
思路:数据量是5000,开始没敢写。。。后来看到解题报告说O(n^2) + 剪枝的话可以通过,然后参考解题报告的思路写的。。。
先按x排序,如果x相等按y排序,然后枚举两个点确定一条直线(这两个点在直线上是相邻的),然后找这条直线上共有多少个这样的点(两相邻点的距离相同。)
已知枚举出i,j点(i为起点);
dx = p[j].x - p[i].x;
dy = p[j].y - p[i].y;
设ans为之前求出的最优值(这里体现出dp的思想)
剪枝:
1、如果p[i].x + ans*dx > R,则跳出这次对j的枚举;
2、如果(p[i].x - dx, p[i].y - dy)在棋盘内,剪枝;
3、如果(p[i].x + ans*dx, p[i].y + ans*dy)不再棋盘内,剪枝;
ps:发现有时候找出的一些不是很隐晦的规律都有可能达到意想不到的效果。。。剪枝博大精深。。
贴代码,79ms
#include#include #include #include #include #define CL(arr, val) memset(arr, val, sizeof(arr))#define REP(i, n) for(i = 0; i < n; ++i)#define FOR(i, l, h) for(i = l; i <= h; ++i)#define FORD(i, h, l) for(i = h; i >= l; --i)#define L(x) x << 1#define R(x) x << 1 | 1#define MID(l, r) (l + r) >> 1#define Min(x, y) x < y ? x : y#define Max(x, y) x < y ? y : x#define E(x) (1 << x)typedef long long LL;using namespace std;const int N = 5050;struct node { int x, y;} p[N];bool mp[N][N];int r, c;bool cmp(node a, node b) { if(a.x == b.x) return a.y < b.y; return a.x < b.x;}bool in(int x, int y) { if(x >= 1 && x <= r && y >= 1 && y <= c) return true; return false;}int cnt(int x, int y, int dx, int dy) { int res = 0; while(in(x, y)) { if(!mp[x][y]) return 0; x += dx; y += dy; ++res; } return res;}int main() { //freopen("data.in", "r", stdin); int n, i, j, dx, dy; int ans = 2, tmp, tx, ty; scanf("%d%d%d", &r, &c, &n); FOR(i, 1, r) FOR(j, 1, c) mp[i][j] = false; REP(i, n) scanf("%d%d", &p[i].x, &p[i].y), mp[p[i].x][p[i].y] = true; sort(p, p + n, cmp); for(i = 0; i < n - 1; ++i) { for(j = i + 1;j < n; ++j) { dx = p[j].x - p[i].x; dy = p[j].y - p[i].y; tx = p[i].x + ans*dx; ty = p[i].y + ans*dy; if(tx > r) break; if(in(p[i].x-dx, p[i].y-dy)) continue; if(!in(tx, ty)) continue; tmp = cnt(p[i].x, p[i].y, dx, dy); ans = Max(ans, tmp); } } if(ans < 3) puts("0"); else printf("%d\n", ans); return 0;}
poj 3280
题意不解释了,很老的回文串问题。。。
dp(i, j)表示i到j区间的最优值,
if(str[l] == str[r]) dp[l][r] = dfs(l + 1, r - 1);else { int a, d; d = min(dfs(l + 1, r) + del[int(str[l])], dfs(l, r - 1) + del[int(str[r])]); a = min(dfs(l + 1, r) + add[int(str[l])], dfs(l, r - 1) + add[int(str[r])]); dp[l][r] = min(a, d);}
贴代码:
#include#include #include #include #include #include #define CL(arr, val) memset(arr, val, sizeof(arr))#define REP(i, n) for(i = 0; i < n; ++i)#define FOR(i, l, h) for(i = l; i <= h; ++i)#define FORD(i, h, l) for(i = h; i >= l; --i)#define L(x) x << 1#define R(x) x << 1 | 1#define MID(l, r) (l + r) >> 1#define Min(x, y) x < y ? x : y#define Max(x, y) x < y ? y : x#define E(x) (1 << x)typedef long long LL;using namespace std;const int N = 200;const int M = 2012;int dp[M][M];int add[N];int del[N];char str[M];char t[10];class Problem {public: Problem() {}; int dfs(int l, int r) { if(l == r) return 0; if(dp[l][r] != -1) return dp[l][r]; if(str[l] == str[r]) dp[l][r] = dfs(l + 1, r - 1); else { int a, d; d = min(dfs(l + 1, r) + del[int(str[l])], dfs(l, r - 1) + del[int(str[r])]); a = min(dfs(l + 1, r) + add[int(str[l])], dfs(l, r - 1) + add[int(str[r])]); dp[l][r] = min(a, d); } return dp[l][r]; } void solve() { int n, m, i, j; int a, b, ans; scanf("%d%d", &n, &m); scanf("%s", str); CL(add, 0); CL(del, 0); REP(i, n) { scanf("%s%d%d", t, &a, &b); add[int(t[0])] = a; del[int(t[0])] = b; } REP(i, m) { REP(j, m) dp[i][j] = (i == j ? 0 : -1); } ans = dfs(0, m - 1); printf("%d\n", ans); }};int main() { //freopen("data.in", "r", stdin); Problem s; s.solve(); return 0;}//ps:class内的数组不能开太大,否则Segmentation fault
poj 2029 (水题)
题意:给一个W*H的棋盘,里边有N个点,给出每个点的坐标。然后给一个小矩形S*T,求S*T这个小矩形最多能圈多少个点。
思路1:预处理,cnt[i][j]表示(1, 1)到(i, j)包含的子矩阵的和。然后O(n^2)枚举小矩阵的左上角的坐标,O(1)求小矩阵的和;
思路2:二维树状数组,把点压入树状数组中。然后也是枚举小矩阵的左上角的坐标,取最优值;
ps:不知道思路1算不算dp,总感觉这个爆搞。。。刚才上网查了一下,某人些的解题报告说是dp,然后我一看写法,尼嘛跟我的思路1一样。。无语了。。。
poj 2948
无比蛋疼的题意!火星上有yeyenum bloggium两种矿石,bloggium只能南北方向运送到最北端,yeyenum只能东西方向运送到最西端。如果这个方格已经建立了东西方向的传送带,则不能再建立南北方向的传送带。已知yeyenum和bloggium的在棋盘上的分布,求运送的最北端和最西端的最大运送量。。。
思路:这题应该算是简单dp,不过本菜无比二货的没想到预处理。已知在纠结怎么一个格子一个格子的转移,思路还是不够开阔。。。-_-!。先预处理mp[0][i][j]表示yeyenum矿石第i行从第1列到第j列的总共有的矿石数。mp[1][i][j]表示bloggium矿石第j列,从第1行到第i行有的矿石数。。。
dp[x][y]表示(1, 1)到(x, y)所包含的区间可以确定的最大矿石数,dp[x][y] = max(dp[x-1][y] + mp[0][x][y], dp[x][y-1] + mp[1][x][y]);
poj 1925
题意:spiderman要去救人,开始在自己的apartment里,需要赶到west tower里。途中有许多building。给出每一个building的位置和高度。spiderman可以涂丝挂到某个building的顶端,然后swing,就像荡秋千一样。求spiderman最少要荡多少次。
思路:这题无比蛋疼的写了两天。第一天想思路,各种错误。本来想好的思路写着写着就发现有bug,根本就不对。。。然后各种改,各种蛋疼!
首先要发现,spiderman的高度始终是h[0],swing的时候是左右对称的。然后应该对x坐标进行dp,本菜开始对数据[0, n)进行dp,本来想写5000*5000的复杂度来着,后来怎么改都不过数据。。。偶很无语,对坐标dp就可以过。。。T_T。
dp[p[i].x + a]到达 p[i].x + a坐标是最少的swing次数。
dp[p[i].x + a] = min(dp[j] + 1) a = p[i].x - j; j = {p[i].x - 1, .... 0}
b = p[i].y - p[0].y; c = p[i].y;
if(c*c < b*b + a*a) break; //这里可以算是一个很效的剪枝。因为j是从打到小枚举的,a是递增的。
贴代码:
#include#include #include #include #include #include #define CL(arr, val) memset(arr, val, sizeof(arr))#define REP(i, n) for(i = 0; i < n; ++i)#define FOR(i, l, h) for(i = l; i <= h; ++i)#define FORD(i, h, l) for(i = h; i >= l; --i)#define L(x) x << 1#define R(x) x << 1 | 1#define MID(l, r) (l + r) >> 1#define Min(x, y) x < y ? x : y#define Max(x, y) x < y ? y : x#define E(x) (1 << x)typedef long long LL;using namespace std;const int inf = ~0u>>2;const int N = 5010;const int M = 1000010;struct node { int x, y;} p[N];int dp[M];LL pre[N];int main() {// freopen("data.in", "r", stdin); //freopen("data.out", "w", stdout); int t, n, i, j, ps; LL a, b, c; scanf("%d", &t); while(t--) { scanf("%d", &n); REP(i, n) scanf("%d%d", &p[i].x, &p[i].y); REP(i, p[n-1].x + 1) dp[i] = inf; dp[0] = 0; FOR(i, 1, n - 1) { b = p[i].y - p[0].y; c = p[i].y; FORD(j, p[i].x - 1, p[0].x) { a = p[i].x - j; if(c*c < a*a + b*b) break; ps = p[i].x + a; if(ps > p[n-1].x) ps = p[n-1].x; if(dp[ps] > dp[j] + 1) dp[ps] = dp[j] + 1; } } printf("%d\n", dp[p[n-1].x] == inf ? -1 : dp[p[n-1].x]); } return 0;}
poj 3034
题意:打地鼠,n*n的棋盘,每秒特定的格子上会有地鼠出没。给一个锤子,比如锤子前一次在(x1, y1),现在移动到(x2, y2)。则它可以打掉(x1, y1) -> (x2, y2)连线上的所有地鼠。锤子每次最多移动的距离为d。求最后最多能打到多少个地鼠。
思路:开始感觉这道题跟黑书上的“舞蹈家怀特先生”有点像,算是多阶段决策。但是没感写,因为在想记忆化搜索的时候越想越乱。。。后来看了一下解题报告,发现有一个地方用的很巧妙,Orz。
dp[t][x][y]表示时间为t是,锤子在(x, y)坐标的最大值。枚举x,y,求t + 1时间到(xx, yy)的最优值, dis(x, y, xx, yy) <= d。在计算(x, y) -> (xx, yy)的时候不能一个单位一个单位的加,否则会超时。dx表示x -> xx的偏移量,dy表示y -> yy的偏移量。然后求出dx, dy的最大公约数tp。 dx /= tp, dy /= tp。这样从(x, y) 到(xx, yy)时没次x += dx, y += dy;
不说了,看代码:
#include#include #include #include #include #include #define CL(arr, val) memset(arr, val, sizeof(arr))#define REP(i, n) for(i = 0; i < n; ++i)#define FOR(i, l, h) for(i = l; i <= h; ++i)#define FORD(i, h, l) for(i = h; i >= l; --i)#define L(x) x << 1#define R(x) x << 1 | 1#define MID(l, r) (l + r) >> 1#define Min(x, y) x < y ? x : y#define Max(x, y) x < y ? y : x#define E(x) (1 << x)typedef long long LL;using namespace std;const int mxn = 60;const int M = 20;struct node { int dx, dy; double dl;} p[mxn<<2];int dp[M][mxn][mxn];int mp[M][mxn][mxn];int K, N;bool cmp(node a, node b) { return a.dl < b.dl;}int gcd(int a, int b) { return b ? gcd(b, a%b) : a;}int get_sum(int t, int x, int y, int xl, int yl, int dx, int dy) { //求t秒时,(x, y)->(xx, yy),每次偏移dx, dy所经过的路线上地鼠的个数 int res = 0; while(1) { res += mp[t][x][y]; if(x == xl && y == yl) break; x += dx; y += dy; } return res;}bool inmap(int x, int y) { if(x >= 0 && x < N && y >= 0 && y < N) return true; return false;}void init() { //预处理可以走的距离 int x, y; K = 0; FOR(x, -5, 5) { // 题目中说d <= 5,因为d可以是反向的,所以是[-5, 5] FOR(y, -5, 5) { if(x*x + y*y <= 25) { p[K].dx = x; p[K].dy = y; p[K++].dl = sqrt(double(x*x) + double(y*y)); } } } sort(p, p + K, cmp);}int iabs(int x) { return x < 0 ? -x : x;}int main() { //freopen("data.in", "r", stdin); init(); int n, d, m; int ans, x, y, i, t, mt; int dx, dy, xl, yl, tp; while(~scanf("%d%d%d", &n, &d, &m)){ if(n + m + d == 0) break; CL(mp, 0); mt = 0; N = n + 2*d + 1; while(m--) { scanf("%d%d%d", &x, &y, &t); mp[t][x+d][y+d] = 1; mt = max(t, mt); } mt ++; CL(dp, 0); FOR(t, 1, mt-1) { REP(x, N) { REP(y, N) { for(i = 0; i < K && p[i].dl <= double(d); ++i) { //枚举这次要求的xx,yy。 dx = p[i].dx; dy = p[i].dy; xl = x + dx; yl = y + dy; if(!inmap(xl, yl)) continue; if(i == 0) ans = mp[t][x][y]; else { tp = gcd(iabs(dx), iabs(dy)); //在求(x,y)->(xx, yy)时所经过的坐标。 dx /= tp; dy /= tp; ans = get_sum(t, x, y, xl, yl, dx, dy); } dp[t+1][xl][yl] = max(dp[t+1][xl][yl], dp[t][x][y] + ans); //dp,取上一步到xx,yy的最优值进行转移 } } } } int ans = 0; for(x = d; x < n + d; ++x) { for(y = d; y < n + d; ++y) { ans = max(ans, dp[mt][x][y]); } } printf("%d\n", ans); } return 0;}
记录状态的动态规划 (状压DP)
POJ 3254
题意:FJ又要搞农场了。这次是一个棋盘,上边有一些地方能种草让牛吃。每一牛分一块地盘。但是牛不喜欢自己的地盘跟别牛的地盘挨着,俗话说的好“卧榻之侧岂容它牛酣睡”。FJ这次又纠结了,他想知道共有多少种方案让所有的牛都满意。
思路:这题跟 很类似,应该算是1185的简化版。O(sum^2 * n)的时间复杂度。chess[i]表示第i行压缩以后的状态,0表示可以选,1表示不能选。status[sum]表示m列中每一个位置都可以选时第sum个状态对应的压缩值;
void init() { int i, j, p; CL(chess, 0); REP(i, n) REP(j, m) {chess[i] <<= 1; chess[i] += (!mp[i][j]);} sum = 0; CL(status, 0); REP(i, E(m)) { p = i; if((p << 1)&i) continue; status[sum++] = i; }}
dp[i][j]表示第i行的状态为j时共有多少种方案。
dp[i][j] = sum(dp[i-1][k]) (status[j] & status[k] == 0);
ans = sum(d[n-1][j])
ps: 注意要取模。我试了一下极限数据 12*12 全为1的情况时会整型溢出。貌似数据并不强0ms过了
POJ 2411
题意:经典的骨牌铺方格,用2*1的骨牌铺满N*M的棋盘。
思路:这题的状态压缩应该跟黑书上的 “BUG‘s 公司”那道题很像。不过这个可以从前一行的状态直接推当前行。也就是说,已知前一行的状态为S’时的最优值是pre,则当前行每一种可以跟S不冲突的状态S的最优值都加上pre。先处理第一行时可行的所有状态,然后枚举后边n-1行,(1<<m)种状态。
dp[i][S]表示前1...i行,并且第i行状态为S时的最优值;
dp[i][S] = sum(dp[i-1][S']) S' 能推到 S;
当然,还有一个很重要的问题就是怎么放骨牌,因为是2*1的骨牌,比如前一种状态为S',S’表示的值为 01101100011011... (0表示没占,1表示已占)显然,S’中含有的0表示要放竖着的骨牌。这样推到的第一个S状态就是把S'按位取反,0变1,1变0。正好是第i行放横放的骨牌跟i-1行一样,然后填上竖放骨牌这种情况。 S = ~S'&((1<<m) - 1);
当然,状态S还可能有很多衍生出来的状态。。。但每个可达的S,都是dp[i][S] += pre; 一次往后推,结果为dp[n-1][(1<<m) - 1],表示刚好填满棋盘。
//已知当前行的状态初始为S,dfs求S的所有衍生状态void dfs(int i, int s, int pos) { if(pos >= m - 1) {dp[i][s] += pre; return ;} dfs(i, s, pos + 1); if(pos <= m - 2 && (s&E(pos + 1)) == 0 && (s&E(pos)) == 0) dfs(i, s|E(pos + 1)|E(pos), pos + 2);}
POJ 1185
详见:
树型动态规划
POJ 2057
题意应该不难懂,虽然很长但是没有太多怪癖的词。说的是一个蜗牛的壳掉在一颗树上了,它要找回他的壳。已知已知这颗树最多有1000个节点,并且有的节点上会有虫子告诉蜗牛它的壳再不在以该节点为根的子树上。求蜗牛找到壳的数学期望。
数学期望求法如下:
如此题目上的图所示。蜗牛从节点1出发(此为树根),可以按照以下的方法找壳:先到2节点,步数是1;如果此处没有壳,她就要返回1,再经由3到达4,步数合计为4(1-2-1-3-4),如果4也没有,她就要返回3再到5,步数是6。合计为11,平均每个节点3.667;
可是如果她先到达3节点,如果4或5都没有,精灵就会告诉她,她就会直接返回1再到2,步数为3;如果4或者5有壳,她到达4的步数为2,到达5的步数为4(1-3-4-3-5),合计为9,平均每个节点为3。事实上这也是最优的。
思路:这题昨天看了一天的解题报告,今天早晨编码才过掉。。。
几个比较好的讲解:
http://hi.baidu.com/billdu/item/da37590ed02350ce74cd3c18
http://hi.baidu.com/lewutian/blog/item/bffdcfdfda4bfe0248540305.html
http://blog.sina.com.cn/s/blog_5f5353cc0100hd08.html
ps:这是道好题,过几天再做一遍。。。
poj 1947
题意,给一颗树,有n个节点,要求从中找到一个含有p个节点的子树(可以删掉一些边)。求要找到这样一个子树最少需要删掉多少边。
思路:f[i][j]表示以i为根的子树保留j个节点最少要cut掉的边数。
f[i][j] = min(f[son(i)][k]);
然后完全就是01背包问题。。。
注意f[i][1]时,从01背包的上一层f'[i][1] + 1得到。
POJ 2486
题意,1为根节点的一颗树。每个节点上有ni个苹果,要求只走k步,最多能摘到多少个苹果。
思路:被这题虐爆了。。。原来以为是简单的树形dp+01背包。。。写完后发现怎么改都不对。。。T_T,然后看了一下以前做的,少考虑一种情况。
f[0][r][k]表示以r为跟,走k步不回到r最多摘得的苹果数。
f[1][r][k]表示以r为根,走k步回到r最多摘得的苹果数。
f[1][r][k] = max(f[1][r][k], f[1][r][k-p] + f[1][son(r)][p-2]); //这个比较好想到,后边的偶就悲剧了;f[0][r][k] = max(f[0][r][k], f[1][r][k-p] + f[0][son(r)][p-2]); //这个也没什么问题吧;
f[0][r][k] = max(f[0][r][k], f[1][son(r)][p-2] + f[0][r][k-p]); //。。。
最后一个转移方程表示从r到son(r)求出f[1][son(r)][..]并回到r,然后再从其它的子树中找不会来的最优值f[0][r][k-p],取其最大;
ps:最后注意的是无向图,建双向边。T_T
POJ 3140
题意很简单,这是道水题,不过几个小trick差点没把我搞死。
先说下思路吧。先求总和sum, 然后再遍历每一个节点dp[r]表示以r为根有多少的子树,总和是多少。
ans = min(ans, iabs((sum - dp[r]) - dp[r]));
从数据看肯定是long long,1、所以ans的初值要在1e13以上。。。2、iabs()函数的返回值类型肯定是long long。。。
贡献好几次wa。。。