赛时没发挥好6题金尾(rank38),剩下很多能写的题,其中四个dp,傻眼ing
The 2019 ICPC Asia Yinchuan Regional Contest
有点迷惑的题,当时看只要 5 5 5 张牌一下子想到暴力枚举,结果发现是不太能行的,导致浪费很多时间。
n n n 张牌,每张牌有 名称,颜色,价值。再给定 5 5 5 个特殊的牌的名称,和一个特殊的牌的颜色。要求从 n n n 张牌中选出 5 5 5 张名称不同的卡牌,基础分数为 5 5 5 张牌的价值之和 s u m sum sum,在此基础上每多一张牌是特殊颜色最终价值加上 0.2 ∗ s u m 0.2*sum 0.2∗sum,每多一张特殊名字牌最终价值加上 0.1 ∗ s u m 0.1*sum 0.1∗sum,最终价值向下取整,求选出的卡牌最大可能的价值。
对于复杂的题目我们先从简单的问题开始考虑。
若是对于该问题只考虑价值之和不考虑附加价值和不能同名的问题是否能解决?就是一个简单的背包,容量为 5 5 5 求最大值的问题。
接下来增加新的信息,若是要求 5 5 5 张牌不能重名如何解决?也很简单,分组背包,同名的牌视作一组,一组内的物品只能选一样的背包问题。
再增加新的信息,普通颜色和普通名称对答案没有任何影响,特殊颜色和名称也只有数量对答案有影响,和具体是什么无关。我们是否能在背包时顺便统计特殊颜色和特殊名称?,也很简单,因为只要选出 5 5 5 张牌,将特殊颜色和特殊名称也当做价值的一种算进dp方程,给两个各多开一维维护即可。
到此问题已经顺利解决,给出dp方程: d p [ i ] [ j ] [ k ] [ p ] : dp[i][j][k][p]: dp[i][j][k][p]: 前 i i i 张卡牌,选出 j j j 张卡牌, k k k 张是特殊颜色, p p p 张是特殊名字卡牌的 合法方案中的最大基数价值。
具体实现可以看代码,有注释和解析。
#include
using namespace std;
#define ll long long
const int N = 1e5 + 10;
int n;
string s[10], color;
map<string, bool> mp;
struct node{
string d, c;
int val;
}cd[N];
bool cmp(node& A, node& B){
return A.d < B.d;
}
int f[N][6][6][6]; // 前 i 张卡牌,选出 j 张卡牌,k 张是特殊颜色,p 张是特殊名字卡牌的 最大基数价值
void cmax(int& a, int b){ a = max(a, b); }
void solve(){
int n;
cin >> n;
for(int i = 1; i <= n; i ++){
cin >> cd[i].d >> cd[i].c >> cd[i].val;
}
mp.clear();
for(int i = 0; i < 5; i ++){
cin >> s[i];
mp[s[i]] = 1;
}
cin >> color;
sort(cd + 1, cd + 1 + n, cmp); // 按名称排序,方便进行类似分组背包的dp
for(int i = 1; i <= n; i ++){
for(int j = 0; j < 6; j ++){
for(int k = 0; k < 6; k ++){
for(int p = 0; p < 6; p ++) f[i][j][k][p] = 0;
}
}
}
int last = 0; // 记录最近的上一个不同名的卡牌为止
for(int i = 1; i <= n; i ++){
if(cd[i].d != cd[i - 1].d) last = i - 1;
int c_v = (cd[i].c == color), s_v = mp.count(cd[i].d); // 这张牌是否是特殊颜色和名称
for(int j = 0; j <= 5; j ++){
for(int k = 0; k <= j; k ++){
for(int p = 0; p <= j; p ++){
// 不取这张牌
cmax(f[i][j][k][p], f[i - 1][j][k][p]); // 可以从同名的卡牌转移 也可以从不同名的转移
// 取这张牌
if((j != 0 && f[last][j][k][p] == 0) || j == 5) continue ; // j = 5 时注意不要越界了
cmax(f[i][j + 1][k + c_v][p + s_v], f[last][j][k][p] + cd[i].val); // 取这一张牌,那么只能从不同名的卡牌转移
}
}
}
}
ll ans = 0;
for(int i = 0; i <= 5; i ++){ // 枚举特殊颜色数量
for(int j = 0; j <= 5; j ++){ // 枚举特殊名字数量
double mul = 0.2 * (double)i + 0.1 * (double)j; // 系数
ans = max(ans, (ll)(mul * (double)f[n][5][i][j]) + f[n][5][i][j]);
}
}
cout << ans << "\n";
}
int main(){
ios::sync_with_stdio(false);
cin.tie(0); cout.tie(0);
int t;
cin >> t;
while(t --){
solve();
}
return 0;
}
给定一个有向图 G G G, x x x 条双向边 y y y 条单向边,可能有负边(保证是单向边)但保证走过单向边 u → v u\rightarrow v u→v 后一定走不回 u u u,求起点 s s s 到所有点的最短路,如果到不了输出 “NO PATH”.
乍一看仿佛很简单,跑个迪杰斯特拉(后文简称djs)最短路不就行了吗?但是写完准备交的时候就发现有坑,因为djs算法是每次从优先队列取出目前为止到起点最近的点 u u u 进行扩展,但是这样的扩展不一定是最优的,但之前扩展过一次下次再想用该点 u u u 扩展就不行了。
考虑这样一个图: 1 1 1 是起点
从 1 → 2 → 4 → 5 1\rightarrow2\rightarrow4\rightarrow5 1→2→4→5 显然是第一轮扩展,在这几个点取出队列之前队首都不会轮到 3 3 3,因为此时 d i s [ 3 ] = 2 > d i s [ 2 ] = 1 > d i s [ 4 ] = 0 > d i s [ 5 ] = 1 dis[3] = 2 > dis[2] = 1 > dis[4] = 0 > dis[5] = 1 dis[3]=2>dis[2]=1>dis[4]=0>dis[5]=1,因为此时这几个点都已经作为过一次扩展点了,之后就不能再次扩展,但是发现当最后点 3 3 3 进行扩展时, 3 → 4 → 5 3\rightarrow4\rightarrow5 3→4→5 会更优秀。
由于题目保证没有负环,我们考虑使用拓扑排序进行优化,先将双向边连接起的连通块用并查集合并,再将单向边连接的这些连通块点集度数 + 1 + 1 +1,但要注意若是有单向边 u → v u\rightarrow v u→v,且 u u u 点是不可从起点到达的,就不用记录该度数。
接下来就是普通的djs算法 + 拓扑排序了,只有当度数为 0 0 0 时才将一个连通块加入优先队列。具体见代码和注释。
#include
using namespace std;
#define ll long long
typedef pair<ll, int> pli;
const int N = 25010;
const ll inf = 1e18;
vector<pair<int, int> > g[N], e[N];
int n, m1, m2, s;
priority_queue<pli, vector<pli>, greater<pli>> q;
ll dis[N];
bool vis[N];
struct DSU {
std::vector<int> f, siz;
DSU() {}
DSU(int maxn) {
init(maxn);
}
void init(int maxn) {
f.resize(++ maxn); // 重构容器大小到 n
std::iota(f.begin(), f.end(), 0); // 批量递增赋值
// siz.assign(maxn, 1); // 赋值n个1
}
int find(int x) {
while (x != f[x]) {
x = f[x] = f[f[x]];
}
return x;
}
bool same(int x, int y) {
return find(x) == find(y);
}
bool merge(int x, int y) {
x = find(x);
y = find(y);
if (x == y) {
return false;
}
f[y] = x;
return true;
}
};
DSU dsu;
int d[N];
vector<int> ID[N];
void djs_top(){
for(int i = 1; i <= n; i ++) dis[i] = inf, vis[i] = 0;
dis[s] = 0;
q.push({0, s});
while(!q.empty()){
auto [di, u] = q.top(); q.pop();
if(vis[u]) continue ;
vis[u] = 1;
// djs
for(auto [v, w] : g[u]){
if(dis[v] > dis[u] + w){
dis[v] = dis[u] + w;
q.push({dis[v], v});
}
}
// 拓扑排序
for(auto [v, w] : e[u]){ // 单向边
int id = dsu.find(v);
d[id] --;
if(dis[v] > dis[u] + w) dis[v] = dis[u] + w;
ID[id].push_back(v); // 存入连通块
if(!d[id]){ // 当连通块度数为0,再将所有块内之前出现过的点存入队列
for(auto x : ID[id]) q.push({dis[x], x});
}
}
}
}
void dfs(int u){
if(vis[u]) return ;
vis[u] = 1;
for(auto [v, w] : g[u]) dfs(v);
for(auto [v, w] : e[u]) dfs(v);
}
int main(){
ios::sync_with_stdio(false);
cin.tie(0); cout.tie(0);
cin >> n >> m1 >> m2 >> s;
dsu.init(n);
for(int i = 1; i <= m1; i ++){
int u, v, w;
cin >> u >> v >> w;
dsu.merge(u, v); // 并查集合并
g[u].push_back({v, w});
g[v].push_back({u, w});
}
for(int i = 1; i <= m2; i ++){
int u, v, w;
cin >> u >> v >> w;
e[u].push_back({v, w});
}
dfs(s); // 将不可达的点判除
for(int i = 1; i <= n; i ++){
if(!vis[i]) continue ;
for(auto [v, w] : e[i]) d[dsu.find(v)] ++; // 连通块的度数 ++
}
djs_top();
for(int i = 1; i <= n; i ++){
if(!vis[i]) cout << "NO PATH\n";
else cout << dis[i] << "\n";
}
return 0;
}
每次给定 n ∗ m n*m n∗m 的矩阵,每个单位代表一个对象(以长度为 k k k 的字符串的形式给出),每次可以删除两个对象(类似连连看,但要求连线必须水平或垂直且最多只能改变一次方向)且路径上不能有未删除的对象(可以有空对象)。
删除两个对象的价值为对应位置上字符相同的数目对应的价值,若相同字符数目为 i i i,价值为 s i s_i si. 题目保证非空对象最多为 18 18 18 个,且为偶数,求可能删除方案的最大价值。
很简单就能想到状压dp,每次枚举两个未删除的非空对象判断是否能连线删除即可,能删除就能转移。难的是处理起来很麻烦,并且题目有 T T T 组样例,要求转移的判断常数要比较小才行。
设检查函数的时间复杂度为 O ( k ) O(k) O(k),状压复杂度 2 18 2^{18} 218,但删除必然是两个一删除,降一阶为 2 17 2^{17} 217,每次需要枚举未删除的两个数,所以总复杂度为 O ( 3407872 ∗ T ∗ k ) O(3407872*T*k) O(3407872∗T∗k).
计算复杂度代码:
int main(){
int cnt = 0;
for(int i = 0; i < (1 << 18); i ++){ // 状压
int x = 0; // 剩余对象的数量
for(int j = 0; j < 18; j ++){
if(!(i >> j & 1)) x ++;
}
if(x & 1) continue ; // 肯定为偶数
cnt += x * (x + 1) / 2 ;
}
cout << cnt;
return 0;
}
我自己代码经过大量预处理,使得 O ( k ) O(k) O(k) 的复杂度降低为当前剩余对象的数量,得以通过此题。具体见代码和注释。
#include
using namespace std;
typedef vector<int> Vec;
typedef pair<int, int> pii;
const int N = (1 << 19), M = 20;
int t, n, m, k;
pii id[M]; // 编号为 i 的对象的坐标
int cr[M][M], val[M], v[M][M]; // cr:坐标为i, j的对象的编号, val:价值, v:编号为i和j的对象一起删除的价值
string s[M][M]; // 对象
int get_id(int x, int y){
return cr[x][y];
}
int f[N]; // 删除状态表示的对象最大价值
// int cmax(int& a, int b){ a = max(a, b); }
bool mp[M][M][M][2]; // 从 i 号到 j 号的两条路径上是否存在 k 号
void checkl(int id1, int id2){
auto [x1, y1] = id[id1];
auto [x2, y2] = id[id2];
// 先向下再向左
for(int i = x1; i <= x2; i ++){
if(s[i][y1][0] != '-') mp[id1][id2][cr[i][y1]][0] = true;
}
for(int j = y1; j >= y2; j --){
if(s[x2][j][0] != '-') mp[id1][id2][cr[x2][j]][0] = true;
}
// 先向左再向下
for(int j = y1; j >= y2; j --){
if(s[x1][j][0] != '-') mp[id1][id2][cr[x1][j]][1] = true;
}
for(int i = x1; i <= x2; i ++){
if(s[i][y2][0] != '-') mp[id1][id2][cr[i][y2]][1] = true;
}
// 将自己编号记为0
mp[id1][id2][id1][0] = mp[id1][id2][id2][0] = 0;
mp[id1][id2][id1][1] = mp[id1][id2][id2][1] = 0;
}
void checkr(int id1, int id2){
auto [x1, y1] = id[id1];
auto [x2, y2] = id[id2];
// 先下再右
for(int i = x1; i <= x2; i ++){
if(s[i][y1][0] != '-') mp[id1][id2][cr[i][y1]][0] = true;
}
for(int j = y1; j <= y2; j ++){
if(s[x2][j][0] != '-') mp[id1][id2][cr[x2][j]][0] = true;
}
// 先右再下
for(int j = y1 + 1; j <= y2; j ++){
if(s[x1][j][0] != '-') mp[id1][id2][cr[x1][j]][1] = true;
}
for(int i = x1 + 1; i <= x2; i ++){
if(s[i][y2][0] != '-') mp[id1][id2][cr[i][y2]][1] = true;
}
// 同理
mp[id1][id2][id1][0] = mp[id1][id2][id2][0] = 0;
mp[id1][id2][id1][1] = mp[id1][id2][id2][1] = 0;
}
void init(){
memset(mp, 0, sizeof mp);
for(int i = 0; i < t; i ++){
for(int j = i + 1; j < t; j ++){
int sum = 0;
auto [x1, y1] = id[i];
auto [x2, y2] = id[j];
for(int r = 0; r < k; r ++) sum += (s[x1][y1][r] == s[x2][y2][r]);
v[i][j] = val[sum]; // 计算价值
if(y2 <= y1) checkl(i, j); // 因为编号大的肯定在下方,但不一定是左边还是右边,分左右分别记录路径
else checkr(i, j);
}
}
for(int i = 1; i < (1 << t); i ++) f[i] = -1;
}
bool check(int id1, int id2, Vec& vis){
int f[] = {1, 1, 1};
for(auto x : vis){ // 查询是否两条路径上都有点未删除
if(mp[id1][id2][x][0]) f[0] = 0;
if(mp[id1][id2][x][1]) f[1] = 0;
if(f[0] == 0 && f[1] == 0) return false;
}
return true;
}
void solve(){
cin >> n >> m >> k;
t = 0;
for(int i = 0; i < n; i ++){
for(int j = 0; j < m; j ++){
cin >> s[i][j];
if(s[i][j][0] != '-'){
cr[i][j] = t;
id[t] = {i, j};
t ++; // 给每个非空对象编号
}
}
}
for(int i = 0; i <= k; i ++) cin >> val[i];
init();
for(int i = 0; i < (1 << t); i ++){
if(f[i] == -1) continue ;
Vec r, g;
for(int j = 0; j < t; j ++){
if(!(i >> j & 1)) g.push_back(j); // 记录未删除的非空对象
}
int siz = g.size();
for(int j = 0; j < siz; j ++){
for(int p = j + 1; p < siz; p ++){ // 枚举两个未删除的对象
if(check(g[j], g[p], g)){ // 未删除,check是否能连线
f[i | (1 << g[j]) | (1 << g[p])] = max(f[i | (1 << g[j]) | (1 << g[p])], f[i] + v[g[j]][g[p]]); // 转移
// cmax(f[i | (1 << j) | (1 << p)], f[i] + d[j][p]); // RE
}
}
}
}
cout << f[(1 << t) - 1] << "\n";
}
int main(){
ios::sync_with_stdio(false);
cin.tie(0); cout.tie(0);
int T;
cin >> T;
while(T --){
solve();
}
return 0;
}