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, 那么容易得到
那么从大到小枚举 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;
}