DP

DP乱选


BZOJ1801: [Ahoi2009]chess 中国象棋

给一个\(N*M(\le 100)\)的棋盘, 放若干个炮, 可以是\(0\)个,使得没有任何一个炮可以攻击另一个炮。请问有多少种放置方法.

Sol:

即每行每列最多放两个

假如记录到现在为止每一列有\(0/1/2\)个炮为当前行的状态, 那么可以枚举\(0/1/2\)个当前行放的炮, 得出上一行的状态, 这样转移

分类讨论一下每种放的数量下有几种转移可能(无非是组合数选一个或两个)

然后发现\(0/1/2\)的个数相同的那些状态, 转移的方案是一样的(数量相同位置不同), 于是可以简化状态了

\(dp[i][j]\)表示当前\(2,1\)的列数分别是\(i,j\), 由于每次一行最多选两个, 又保证了上一状态列上的个数不是负数, 所以得到都是合法方案

f[0][0][0] = 1;
    for (LL i = 1; i <= n; ++ i)
    {
        for (LL x = 0; x <= m; ++ x)
        {
            for (LL y = 0; y <= m - x; ++ y)
            {
                // put 0
                (f[i][x][y] += f[i - 1][x][y]) %= MOD;
                // put 1
                if (y >= 1) (f[i][x][y] += f[i - 1][x][y - 1]     * c[m - x - y + 1][1] % MOD) %= MOD;
                if (x >= 1) (f[i][x][y] += f[i - 1][x - 1][y + 1] * c[y + 1][1]         % MOD) %= MOD;
                // put 2
                if (y >= 2) (f[i][x][y] += f[i - 1][x][y - 2]         * c[m - x - y + 2][2] % MOD) %= MOD;
                if (x >= 2) (f[i][x][y] += f[i - 1][x - 2][y + 2]     * c[y + 2][2]         % MOD) %= MOD;
                if (x >= 1) (f[i][x][y] += f[i - 1][x - 1][y - 1 + 1] * c[y][1] % MOD * c[m - x + 1 - y][1] % MOD) %= MOD;
                if (i == n) (ans += f[i][x][y]) %= MOD;
            }
        }
    }

LuoguP1860 新魔法药水

(题面有点烦)

\(N(\le 60)\)种药水, 给定买药水的售价和卖药水的回收价

\(M(\le 240)\)种使用魔法的药水配方(一种药水由其他若干种不同药水组成), 你可以通过购买原料药水, 并花费若干次魔法合成一种药水, 来赚取差价

先规定初始有\(V\)元, 所有原料药水必须在初始购买, 每种魔法可以重复使用, 产生的药水可以作为下一次魔法的原料(这样就少购买), 但最多只能用\(K\)

问最大赚取的差价是多少?

Sol:

一开始考虑到可能有环, 那就不能直接在图上跑了

但是如果设置这样一个状态\(f[i][j][k]\)表示前\(i\)种魔法, 花费\(j\)元, 花费\(k\)魔法的最大收益, 那么状态之间的转移是不会有环的

所以直接考虑DP这个\(f[i][j][k]\)即可

hint: 不是直接转移而是间接转移

#include <iostream>
#include <cstdio>
#include <cstring>
#include <algorithm>
#include <cmath>
  
using namespace std;
typedef long long LL;
const LL INF = 2e9;
const int MAXN = 62, MAXV = 1002, MAXK = 32, MAXM = 242;
  
int n, m, V, K;
 
LL ans;
LL cost[MAXN], val[MAXN];
LL mag[MAXM][MAXN], to[MAXM], num[MAXM];
LL minc[MAXM][MAXK], ming[MAXM][MAXN][MAXK];
LL f[MAXV][MAXK];
void init()
{
    memset(minc, 0x3f3f3f, sizeof minc);
    memset(ming, 0x3f3f3f, sizeof ming);
    for (int i = 1; i <= n; ++ i) minc[i][0] = cost[i];
    for (int i = 0; i <= m; ++ i)
        for (int j = 0; j <= K; ++ j)
            ming[i][0][j] = 0;
    for (int i = 1; i <= K; ++ i)
    {
        for (int j = 1; j <= m; ++ j)
        {
            for (int k = 1; k <= num[j]; ++ k) // 第j种魔法, 前k个, 消耗i次的最小花费
                for (int p = 0; p <= i - 1; ++ p)
                    ming[j][k][i - 1] = min(ming[j][k][i - 1], ming[j][k - 1][i - 1 - p] + minc[mag[j][k]][p]);
            minc[to[j]][i] = min(minc[to[j]][i], ming[j][num[j]][i - 1]);
        }
    }
}
 
int main()
{
    scanf("%d%d%d%d", &n, &m, &V, &K);
    for (int i = 1; i <= n; ++ i) scanf("%lld%lld", &cost[i], &val[i]);
    for (int i = 1; i <= m; ++ i)
    {
        scanf("%lld%lld", &to[i], &num[i]);
        for (int j = 1; j <= num[i]; ++ j) scanf("%lld", &mag[i][j]);
    }
    init();
    for (int i = 1; i <= n; ++ i)
    {
        for (int j = 0; j <= V; ++ j)
        {
            for (int k = 0; k <= K; ++ k)
            {
                for (int p = 0; p <= k; ++ p)
                    if (j >= minc[i][p] && k >= p)
                        f[j][k] = max(f[j][k], f[j - minc[i][p]][k - p] + val[i] - minc[i][p]);
                ans = max(ans, f[j][k]);
            }
        }
    }
    printf("%lld\n", ans);
    return 0;
}

BZOJ1833: [ZJOI2010]count 数字计数

Description

​ 给定两个正整数a和b,求在[a,b]中的所有整数中,每个数码(digit)各出现了多少次。

Input

​ 输入文件中仅包含一行两个整数a、b,含义如上所述。

Output

​ 输出文件中包含一行10个整数,分别表示0-9在[a,b]中出现了多少次。

Sample Input

​ 1 99

Sample Output

​ 9 20 20 20 20 20 20 20 20 20

HINT

​ 30%的数据中,a<=b<=10^6;
​ 100%的数据中,a<=b<=10^12。

Source

Day1

全网最菜做法

\(dp[i][j][k][0]\)表示到第\(i\)位为止(从后往前), 数码\(j\)出现了\(k\)次, 有没有上限限制, 的数的个数

非零数码很好转移, \(0\)不能有前导的怎么办?, 其实只要把前导\(0\)算作\(0\)出现\(0\)次即可(本质上也就是这样), 其他转移跟普通的一样, 涉及到\(dp[i][0][0][0]\)\(-1\)即可, 因为前导全是\(0\)的算在里面

//又丑又长的代码
LL dgt[20];
int getdgt(LL x)
{
	int ret = 0;
	while (x)
	{
		dgt[++ ret] = x % 10;
		x /= 10;
	}
	return ret;
}
LL dp[14][10][14][2];
LL cnt[20];
void solve(LL x, bool flag) // 到第i位, j出现了k次, 是否有约束的数的个数
{
    if (x == 0LL) return ;
	memset(dp, 0, sizeof dp);
	int len = getdgt(x);
	for (int i = 0; i <= 9; ++ i)
		dp[len + 1][i][0][1] = 1;
    for (int i = len; i >= 1; -- i) // 12
	{
		for (LL j = 0; j <= 9; ++ j) // 10
		{
            if (j == 0) dp[i][j][0][0] += 1;
            dp[i][j][0][0] += dp[i + 1][j][0][0] * 9;
            dp[i][j][0][0] += dp[i + 1][j][0][1] * 1LL * (dgt[i] - (j < dgt[i]));
            dp[i][j][0][1] += dp[i + 1][j][0][1] * (j != dgt[i]);
			for (int k = 1; k <= len - i + 1; ++ k) // 12
			{
                if (k == len - i + 1 && j == 0) continue;
				dp[i][j][k][0] += dp[i + 1][j][k][0] * 1LL * 9 + dp[i + 1][j][k - 1][0];
                if (j == 0 && k == 1) dp[i][j][k][0] -= 1LL;
				dp[i][j][k][0] += dp[i + 1][j][k][1] * 1LL * (dgt[i] - (j < dgt[i])) + dp[i + 1][j][k - 1][1] * 1LL * (j < dgt[i]);
				dp[i][j][k][1] += dp[i + 1][j][k][1] * 1LL * (j != dgt[i]) + dp[i + 1][j][k - 1][1] * 1LL * (j == dgt[i]);
			}
		}
	}
	for (int i = 0; i <= 9; ++ i) 
	{
		LL sum = 0;
		for (LL j = 0; j <= len; ++ j) sum += (dp[1][i][j][0] + dp[1][i][j][1]) * j;
        if (flag) cnt[i] += sum;
        else cnt[i] -= sum;
	}
}

int main()
{
	LL A = in(), B = in();
	solve(B, 1); solve(A - 1, 0);
    for (int i = 0; i <= 9; ++ i) printf("%lld ", cnt[i]);
	return 0;
}

其实把手算的过程模拟一遍也行, 小学数数也行

BZOJ 2111: [ZJOI2010]Perm 排列计数

Description

称一个\(1,2,...,N\)的排列\(p_1,p_2...,p_n\)是Magic的,当且仅当\(2\le i\le N\)时,\(p_i>p_{i/2}\). 计算\(1,2,...N\)的排列中有多少是Magic的,答案可能很大,只能输出模\(P\)以后的值

Input

输入文件的第一行包含两个整数 n和p,含义如上所述。

Output

输出文件中仅包含一个整数,表示计算\(1,2,⋯, N\)的排列中, Magic排列的个数模 \(P\)的值。

Sample Input

20 23

Sample Output

16

HINT

\(100\%\)的数据中,\(1 ≤ N ≤ 10^6, P ≤ 10^9\)\(P\)是一个质数。 数据有所加强

Sol:

理应看到 "\(p_i>p_{i/2}\)"就要想到树的, 然而我在傻逼达标找规律

排列中各大小关系可以表示成一颗以 \(1\)为根, 节点\(x\)的左右儿子分别为\(2x,2x+1\)的这样一颗完全二叉树, 父亲小于儿子

然后发现一个树的答案和具体包含那些数值无关, 只和大小有关(相当于离散一下)

那么一个根, 对于两个子树内部的问题是完全独立的, 可以当做一个大小即为子树大小的子问题

所以组合数一下分配给左右子树的数的集合即可即\(F[i]=comb(i-1, p)*F[p]*F[i-1-p]\), 由于数的形态固定, 这个\(p\)是可以求的

int n;
LL p;
int nex[MAXN];
LL fac[MAXN], ifac[MAXN];
int getnex(int x)
{
	int ret = 1, k = 1, now;
	while ((2 << k) - 1 <= x) ++ k;
	now = (1 << k) - 1;
	if (x - now < (1 << (k - 1))) return (1 << (k - 1)) - 1;
	else return (2 << (k - 1)) - 1;
}
LL exgcd(LL a, LL b, LL & x, LL & y)
{
	if (!b) 
	{
		x = 1, y = 0;
		return a;
	}
	LL d = exgcd(b, a % b, x, y);
	x -= (a / b) * y;
	swap(x, y);
	return d;
}
LL inv(LL a)
{
	LL x, y;
	exgcd(a, p, x, y);
	return (x % p + p) % p;
}
LL comb(LL a, LL b) // a \ge b
{
	return fac[a] * ifac[a - b] % p * ifac[b] % p;
}
void init()
{
	for (int i = 1; i <= n; ++ i) nex[i] = getnex(i);
	fac[0] = ifac[0] = 1;
	for (int i = 1; i <= n; ++ i)
	{
		fac[i] = fac[i - 1] * 1LL * i % p;
		ifac[i] = inv(fac[i]);
	}
}

LL f[MAXN];
LL solve(LL x)
{
	if (f[x]) return f[x];
	return f[x] = comb(x - 1, nex[x]) * solve(nex[x]) % p * solve(x - nex[x] - 1) % p;
}

int main()
{
	n = in(); p = in();
	init();
	f[1] = 1; f[2] = 1;
	printf("%lld\n", solve(n));
	return 0;
}

2298: [HAOI2011]problem a

Description

一次考试共有\(n\)个人参加,第\(i\)个人说:“有\(a_i\)个人分数比我高,\(b_i\)个人分数比我低。”问最少有几个人没有说真话(可能有相同的分数)

Input

第一行一个整数\(n\),接下来\(n\)行每行两个整数,第\(i+1\)行的两个整数分别代表\(a_i,b_i\)

Output

一个整数,表示最少有几个人说谎

Sample Input

3

2 0

0 2

2 2

Sample Output

1

HINT

100%的数据满足: 1≤n≤100000 0≤ai、bi≤n

Sol:

首先将\(a_i, b_i\)转化成\((b_i,n-a_i)\), 这样就代表\(<\)自己的个数, \(\le\)自己的个数

那么这样一个二元组唯一对应一种分数, 并且两个二元组之间要么完全相同, 要么一个的前一个数和另一个的后一个数相同

然后想啊想, 用前缀和? 差分? 排名? 线段!

把上述的二元组放在数轴上, 要求的就是最多能修改的线段数

首先如果同一个线段重复次数超过它本身的长度, 那就减去多余的部分, 因为既然存在的次数超限了, 不管怎么修改其他种类的线段都不行, 只能把自己多余的部分修改, 必须修改

那么这样所有线段的重复次数就合法了, 要求最多不修改的数量, 就是要求最多保留线段, 使他们不想交或重叠(可以收尾相接), 这样就可做了

struct Seg
{
	int l, r, rpt;
} seg[MAXN], tmp[MAXN];
bool cmp(Seg a, Seg b)
{
	return (a.r == b.r) ? (a.l < b.l) : (a.r < b.r);
}

void init()
{
	sort(seg + 1, seg + n + 1, cmp);
	int realn = 0;
	for (int i = 1; i <= n; ++ i)
	{
		if (i == 1 || (seg[i].l != seg[i - 1].l || seg[i].r != seg[i - 1].r))
			tmp[++ realn] = seg[i];
		else 
			++ tmp[realn].rpt;
	}
	n = realn;
	for (int i = 1; i <= n; ++ i) 
	{
		seg[i] = tmp[i];
		if (seg[i].rpt > seg[i].r - seg[i].l)
			seg[i].rpt = seg[i].r - seg[i].l;
	}
}

int f[MAXN];

int main()
{
	n = in(); m = n;
	for (int i = 1; i <= n; ++ i)
	{
		int a = in(), b = in();
		seg[i] = (Seg) { b, n - a, 1 };
	}
	init();
	int las = 0;
	for (int i = 1; i <= n; ++ i)
	{
		for (int j = las + 1; j <= seg[i].r; ++ j)
			f[j] = max(f[j], f[j - 1]);
		f[seg[i].r] = max(f[seg[i].r], f[seg[i].l] + seg[i].rpt);
		las = seg[i].r;
	}
	printf("%d\n", m - f[seg[n].r]);
	return 0;
}

NOIP2017D2T2 宝藏

题意

\(n \le 12\) 有边权的无向图, 定义一颗生成树权值为 边权*深度 的和
求最小的生成树

Sol;

状压, 但是我不会准确证明复杂度, 只是差不多能卡过去

f[u][dep][sta] 表示这个点 u 距离选的根深度为 dep, 已选的点集为 sta (生成树上 u 的子树), 以这个点为子树根的最小代价
所以只要枚举儿子 v , 和儿子的点集 res , res 是 sta 的子集,
设 pre = sta - res, 那么容易得到

\[f[u][dep][sta] = \min\{ f[u][dep][pre] + f[v][dep + 1][res] + dis[u][v] * dep \} \]

那么从大到小枚举 dep, 从小到大枚举 sta, 就可以保证子状态都已经计算到

如果采用高效的枚举子集, 枚举子集内的元素的方法, 那么复杂度应该是 深度 * 点 * 枚举子集的子集 * 点, 也就是 \(O(n ^ 3 * 3 ^ n)\)
但是其中有些状态是无意义的, 比如深度很大, 而点集已经选满, 或者枚举的点不在集合里
我用了记搜来避免上述两个无意义的枚举

然后还有玄学剪枝, 即若某个 f 值已经 > ans 了, 那么把他设为极大值, 下次枚举到后继状态是他自然会 continue

PS: 除了交了一发假算法, 没有用 lowbit 之类的之前也能 90~95 , 应该是差不多了吧

int n, m, K;

int wgt[13][13];
int siz[(1 << 12) + 10], lowb[(1 << 12) + 10], getd[(1 << 12) + 10];

int lowbit(int & x) { return x & (-x); }

int f[13][13][(1 << 12) + 10];
const int INF = 1e9;
int ans = INF;
void DFS(int u, int dep, int sta)
{
//    printf("%d %d %d\n", u, dep, sta);
    if (f[u][dep][sta] < INF) return ;
    int tmp = sta ^ (1 << (u - 1));
    for (int s = (tmp - 1) & tmp; ; s = (s - 1) & tmp)
    {
	int pre = s + (1 << (u - 1));
	DFS(u, dep, pre);
	if (f[u][dep][pre] >= f[u][dep][sta]) 
	{
	    if (!s) break;
	    continue;
	}
	int res = sta - pre;
	for (int S = res; S; S ^= lowb[S])
	{
	    int v = getd[lowb[S]] + 1;
	    if (wgt[u][v] == -1) continue;
	    DFS(v, dep + 1, res);
	    if (f[v][dep + 1][res] >= INF-1) continue;
	    f[u][dep][sta] = min(f[u][dep][sta], f[u][dep][pre] + f[v][dep + 1][res] + wgt[u][v] * dep);
	}
	if (!s) break;
    }
    if (f[u][dep][sta] == INF) f[u][dep][sta] --;
    if (sta == (1 << n) - 1) ans = min(ans, f[u][dep][sta]);
    if (f[u][dep][sta] >= ans) f[u][dep][sta] = INF - 1;
}
/*
  0741
*/
int main()
{
    n = in(); m = in();
    memset(wgt, -1, sizeof wgt);
    for (int i = 1; i <= m; ++ i)
    {
	int u = in(), v = in(), w = in();
	if (wgt[u][v] == -1 || w < wgt[u][v]) wgt[u][v] = wgt[v][u] = w;
    }
    for (int i = 0; i < n; ++ i) getd[1 << i] = i;
    for (int i = 1; i < (1 << n); ++ i) 
    {
	lowb[i] = lowbit(i);
	siz[i] = siz[lowb[i]] + 1;
    } 
    for (int i = 0; i <= n; ++ i)
	for (int k = 1; k <= n; ++ k)
	    for (int j = 0; j < (1 << n); ++ j)
		f[i][k][j] = INF;
    for (int i = 1; i <= n; ++ i) 
	for (int j = 1; j <= n; ++ j) 
	    f[i][j][1 << (i - 1)] = 0;
    for (int i = 1; i <= n; ++ i) DFS(i, 1, (1 << n) - 1);
    if (ans == INF) ans = 0;
    printf("%d\n", ans);
    return 0;
}
posted @ 2019-11-14 15:11  Kuonji  阅读(441)  评论(0编辑  收藏  举报
nmdp