POI 2015 精选算法题解析
矩阵倍增与状态扩展
针对图中路径计数问题,若所有边的权重统一为单位长度,计算特定步数内的可达性通常直接应用矩阵乘法结合二分加速。为了处理非单位权重的特殊情况,可以通过构建虚拟汇点并引入自环来标准化图结构。在此类权重范围为 [1, 3] 的场景中,传统的倍增法需要进行节点分裂(State Splitting)。将原始节点 $u$ 扩展为三个状态子节点 $(u,0), (u,1), (u,2)$,分别代表当前距离模 3 后的余数偏移量。每条实际边 $(u, v)$ 权重为 $w$ 时,转化为从 $(u, w \pmod 3)$ 指向 $(v, 0)$ 的映射关系。
通过构建增广邻接矩阵并利用二进制拆分思想进行预计算,可以高效求解满足路径总数阈值的最小步数。具体实现中定义了矩阵乘法运算及状态判定函数,利用滚动数组更新中间结果。
#include <iostream>
#include <cstdio>
#include <vector>
#include <algorithm>
using namespace std;
typedef long long ll;
// 定义矩阵结构体
struct Matrix {
ll mat[150][150];
Matrix() {
for(int i=0; i<150; ++i)
for(int j=0; j<150; ++j)
mat[i][j] = 0;
}
};
int N, M;
ll K;
Matrix base_pow[70];
Matrix mul(const Matrix& A, const Matrix& B) {
Matrix C;
for(int i=1; i<=N*3; ++i) {
for(int j=1; j<=N*3; ++j) {
for(int k=1; k<=N*3; ++k) {
C.mat[i][j] += A.mat[i][k] * B.mat[k][j];
}
}
}
return C;
}
bool check(Matrix& A) {
ll total = 0;
for(int i=1; i<=N; ++i) {
// 检查是否满足达到目标次数的条件
if(total >= K - (A.mat[i][0] - 1)) return true;
total += A.mat[i][0] - 1;
}
return false;
}
inline int read_int() {
int x=0, f=1; char c=getchar();
while(c<'0' || c>'9') {if(c=='-')f=-1;c=getchar();}
while(c>='0' && c<='9') {x=x*10+(c-'0');c=getchar();}
return x*f;
}
int main() {
N = read_int(); M = read_int(); scanf("%lld", &K);
// 初始化基础矩阵
for(int i=1; i<=N; ++i) {
base_pow[0].mat[0][i+n] = 0; // Placeholder for virtual node mapping logic
// Setup self-loops for expanded nodes
base_pow[0].mat[i+n][i+n*2+1] = 1;
base_pow[0].mat[i+n*2][i+n*2] = 1;
}
// Note: Specific mapping logic adapted to simplified structure
for(int i=1; i<=M; ++i) {
int u = read_int(), v = read_int(), w = read_int();
base_pow[0].mat[u+(w-1)*N][v]++;
}
// Binary lifting preparation
bool found = false;
int pos = -1;
for(int i=1; i<=63; ++i) {
base_pow[i] = mul(base_pow[i-1], base_pow[i-1]);
if(check(base_pow[i])) {
pos = i;
found = true;
break;
}
}
if(!found) { puts("-1"); return 0; }
ll ans = 0;
Matrix res;
// Identity initialization
for(int i=1; i<=N; ++i) res.mat[i][i] = 1;
for(int i=pos-1; i>=0; --i) {
Matrix next_st = mul(res, base_pow[i]);
if(!check(next_st)) {
res = next_st;
ans += (1LL << i);
}
}
printf("%lld\n", ans);
return 0;
}
区间动态规划优化
对于涉及区间赋值最大收益的问题,可以使用区间 DP 建模。定义状态 $dp_{l,r,val}$ 表示在闭区间 $[l,r]$ 内取到的最小值恰好为 $val$ 时的最大获利。首先需要对输入的成本参数进行坐标离散化,以便压缩状态空间。
转移方程的核心在于枚举区间最小值的所在位置 $pos$,并将原问题分解为左右两个子区间 $[l, pos-1]$ 和 $[pos+1, r]$。由于子问题需要满足最小值不小于当前层级,可以引入辅助状态 $f_{l,r,i}$ 来表示 $dp_{l,r,j}$ 在 $j \ge i$ 时的最大值,从而避免重复枚举,将复杂度从三维降低。
#include <iostream>
#include <cstdio>
#include <cstring>
#include <vector>
#include <algorithm>
using namespace std;
typedef long long ll;
const int MAXN = 10010;
const int MAXM = 4100;
struct Constraint {
int l, r, cost_idx;
} constraints[MAXM];
int n, m, val_cnt;
int raw_vals[MAXM];
int best_profit[MAXN][MAXN][MAXM];
int suffix_max[MAXN][MAXN][MAXM];
int decision_point[MAXN][MAXN][MAXM];
int final_assignment[MAXN];
int fast_read() {
int x=0, sgn=1; char ch=getchar();
while(ch<'0'||ch>'9'){if(ch=='-')sgn=-1;ch=getchar();}
while(ch>='0'&&ch<='9'){x=x*10+(ch-'0');ch=getchar();}
return x*sgn;
}
void reconstruct_dp(int l, int r, int k_idx) {
if(l > r) return;
if(suffix_max[l][r][k_idx] == 0) {
for(int i=l; i<=r; ++i) final_assignment[i] = raw_vals[k_idx];
return;
}
for(int idx=k_idx; idx<=val_cnt; ++idx) {
if(best_profit[l][r][idx] == suffix_max[l][r][k_idx]) {
int pivot = decision_point[l][r][idx];
final_assignment[pivot] = raw_vals[idx];
reconstruct_dp(l, pivot-1, idx);
reconstruct_dp(pivot+1, r, idx);
break;
}
}
}
int main() {
n = fast_read(); m = fast_read();
vector<int> sorted_unique;
sorted_unique.reserve(m);
for(int i=1; i<=m; ++i) {
constraints[i].l = fast_read();
constraints[i].r = fast_read();
constraints[i].cost_idx = fast_read();
sorted_unique.push_back(constraints[i].cost_idx);
}
sort(sorted_unique.begin(), sorted_unique.end());
val_cnt = unique(sorted_unique.begin(), sorted_unique.end()) - sorted_unique.begin();
// Map costs to rank
for(int i=1; i<=m; ++i) {
constraints[i].cost_idx = lower_bound(sorted_unique.begin(), sorted_unique.end(), constraints[i].cost_idx) - sorted_unique.begin();
}
// Precompute counts for efficiency
int freq[MAXN][MAXM];
memset(freq, 0, sizeof(freq));
for(int len_l=n; len_l>=1; --len_l) {
for(int len_r=len_l; len_r<=n; ++len_r) {
// Update local counts g[i][j]
// ... (Logic compressed for clarity)
for(int i=len_l; i<=len_r; ++i) {
for(int j=val_cnt; j>=1; --j) {
int gain = suffix_max[len_l][i-1][j] + suffix_max[i+1][len_r][j];
// Logic dependent on coverage count g[i][j]
int potential = gain + /*calc_coverage_gain*/(i, j, constraints, n);
if(potential > best_profit[len_l][len_r][j]) {
best_profit[len_l][len_r][j] = potential;
decision_point[len_l][len_r][j] = i;
}
suffix_max[len_l][len_r][j] = max(suffix_max[len_l][len_r][j+1], best_profit[len_l][len_r][j]);
}
}
}
}
reconstruct_dp(1, n, 0);
printf("%d\n", suffix_max[1][n][0]);
for(int i=1; i<=n; ++i) printf("%d ", final_assignment[i]);
return 0;
}
基于线段树的动态最大子段和
此类动态维护问题可转化为带修改的最大子段和查询。核心思想是利用前缀和处理元素贡献:当某个数值首次出现在序列中时,其对应位置贡献为正权值;第二次出现时,为避免重复计算,贡献为负权值以抵消之前的记录;第三次及以上则不再影响最大子段和的计算。
我们需要维护一个线段树,每个节点存储当前区间的总和、从左端点延伸的最大值、从右端点延伸的最大值以及该区间内的全局最大子段和。每次移动左边界或删除元素时,使用单点修改操作动态调整相关位置的权值。
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
typedef long long ll;
struct NodeInfo {
ll total_sum;
ll left_max;
ll right_max;
ll max_subsegment;
} tree_data[4004005];
int arr_vals[1001005];
int occurrences_count[1001005];
int positions_of_val[1001005][2]; // Store up to first two positions
vector<int> value_positions[1001005];
int n, q, vals_limit;
void push_up(int node) {
tree_data[node].total_sum = tree_data[node*2].total_sum + tree_data[node*2+1].total_sum;
tree_data[node].left_max = max(tree_data[node*2].left_max, tree_data[node*2].total_sum + tree_data[node*2+1].left_max);
tree_data[node].right_max = max(tree_data[node*2+1].right_max, tree_data[node*2].right_max + tree_data[node*2+1].total_sum);
tree_data[node].max_subsegment = max({tree_data[node*2].max_subsegment, tree_data[node*2+1].max_subsegment, tree_data[node*2].right_max + tree_data[node*2+1].left_max});
}
void update_point(int node, int l, int r, int idx, ll val) {
if(l == r) {
tree_data[node] = {val, val, val, val};
return;
}
int mid = (l + r) >> 1;
if(idx <= mid) update_point(node*2, l, mid, idx, val);
else update_point(node*2+1, mid+1, r, idx, val);
push_up(node);
}
int main() {
int rd_n = fast_read();
int rd_q = fast_read();
// Reading input and preprocessing positions
// ... Initialization omitted for brevity
// Assume 'arr_vals', 'positions_of_val', 'value_positions' filled correctly
ll ans = 0;
// Initial setup for max subarray calculation
// ... Apply initial weights based on frequency logic
for(int i=1; i<=rd_n; ++i) {
ans = max(ans, tree_data[1].max_subsegment);
// Reset current position weight to 0 as it moves out
update_point(1, 1, rd_n, i, 0);
occurrences_count[arr_vals[i]]++;
int cnt = occurrences_count[arr_vals[i]];
// Adjust weights for next appearances of the same value
// If there is another instance later, restore its positive weight or set negative based on logic
// Specific logic adapts to the constraint requirements
}
printf("%lld\n", ans);
return 0;
}
双树状数组的条件求和
在处理具有大小约束的元素分配问题时,可以将大于阈值的独立数量与小于阈值的填充需求分开统计。记大于等于阈值 $s$ 的元素个数为 $count_{\ge s}$,剩余的空位需由较小元素填补。
若小元素的总和不足以填满剩余空位乘以单个容量的积,则任务无法完成。为了高效支持单点修改和区间查询,我们使用两棵独立的树状数组(BIT):第一棵统计指定范围内元素的总个数,第二棵维护这些元素的数值之和。
#include <iostream>
#include <cstdio>
#include <vector>
#include <algorithm>
using namespace std;
typedef long long ll;
struct QueryNode {
int op_type, param1, param2;
};
struct BinaryIndexedTree {
ll bit_map[2000005];
int size_limit;
void init(int n) {
size_limit = n;
fill(begin(bit_map), end(bit_map), 0);
}
void add_val(int index, ll delta) {
for(; index <= size_limit; index += index & (-index)) {
bit_map[index] += delta;
}
}
ll query_prefix(int index) {
ll result = 0;
for(; index > 0; index -= index & (-index)) {
result += bit_map[index];
}
return result;
}
} count_tree, sum_tree;
ll fast_read_ll() {
char ch=getchar(); ll x=0;
while(ch<'0'||ch>'9') ch=getchar();
while(ch>='0'&&ch<='9'){x=x*10+(ch-'0');ch=getchar();}
return x;
}
int main() {
int n = (int)fast_read_ll();
int m = (int)fast_read_ll();
char operation_str[5];
vector<QueryNode> history;
history.resize(m + 1);
vector<int> distinct_values;
distinct_values.reserve(m);
for(int i=1; i<=m; ++i) {
scanf("%s", operation_str);
history[i].op_type = (operation_str[0]=='U'?0:1);
history[i].param1 = (int)fast_read_ll();
history[i].param2 = (int)fast_read_ll();
distinct_values.push_back(history[i].param2);
}
sort(distinct_values.begin(), distinct_values.end());
auto end_iter = unique(distinct_values.begin(), distinct_values.end());
int unique_count = distance(distinct_values.begin(), end_iter);
// Coordinate compression
// ... Re-map history[i].param2 to ranks
// Initialize trees
// ...
// Process queries
// Logic: Calculate required space based on items >= threshold
// Compare sum of items < threshold against requirement
return 0;
}