SingleGreedy
Move Type: Basic Complexity: O(1) to O(objects × containers) with early termination
Stop as soon as ANY improving move is found. Ultra-fast convergence, but may miss better moves. Use when speed matters more than solution quality.
Overview
SingleGreedy evaluates single object moves but stops immediately when it finds the first move that improves the objective. Unlike SingleFast which explores at least one object fully, SingleGreedy can stop even earlier - mid-way through exploring destinations for a single object.
Use when:
- Speed is critical (interactive/real-time systems)
- Quick "good enough" solution needed
- Problem updates frequently (re-runs are cheap)
- Initial assignment is already decent
Avoid when:
- Solution quality is important
- Making many moves at once (may thrash)
- Problem is small (overhead dominates)
- Need deterministic/reproducible results
Quick Example
- Python
- C++
# Ultra-fast: stops at first improving move
solver.addSolver(
SolverSpecs(
localSearchSolverSpec=LocalSearchSolverSpec(
moveTypeList=[
SingleGreedyMoveTypeSpec(), # Fastest convergence
]
)
)
)
void quickExample() {
// Ultra-fast: stops at first improving move
auto executor = std::make_shared<folly::CPUThreadPoolExecutor>(4);
ProblemSolver solver(executor, "example", "test");
solver.setObjectName("task");
solver.setContainerName("host");
LocalSearchSolverSpec solverSpec;
solverSpec.moveTypeList()->emplace_back();
solverSpec.moveTypeList()->back().singleGreedyMoveTypeSpec() =
SingleGreedyMoveTypeSpec{};
solver.addSolver(solverSpec);
// Setup problem
solver.setAssignment(
std::map<std::string, std::vector<std::string>>{
{"host0", {"task0", "task1", "task2"}},
{"host1", {}},
{"host2", {}},
});
solver.addObjectDimension(
"cpu",
std::map<std::string, double>{
{"task0", 1.0},
{"task1", 1.0},
{"task2", 1.0},
});
BalanceSpec balanceSpec;
balanceSpec.name() = "balance-cpu";
balanceSpec.scope() = "host";
balanceSpec.dimension() = "cpu";
solver.addGoal(balanceSpec, 1.0);
// Solve and print results
auto solution = solver.solve();
std::cout << " Initial objective: " << *solution.initialObjective()->value()
<< "\n";
std::cout << " Final objective: " << *solution.finalObjective()->value()
<< "\n";
std::cout << " Improvement: "
<< (*solution.initialObjective()->value() -
*solution.finalObjective()->value())
<< "\n";
}
Parameters
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
| (none) | - | - | - | SingleGreedy has no configuration parameters |
How It Works
Given a hot container (the container contributing most to the objective):
- Select object: Pick object from hot container
- Try destination: Pick a different container
- Evaluate move: Test moving object to that container
- Early exit: If move improves objective → STOP and apply it
- Continue: Otherwise, try next destination or next object
- Worst case: If no improving move found, try all combinations
Visual Example
Hot Container (100 objects):
obj1 → container_A: improvement found! ✓ STOP HERE
SingleGreedy returns after evaluating just 1 move!
Comparison with other move types:
- SingleGreedy: Tries 1 move, finds improvement, stops
- SingleFast: Tries 1000 moves (all destinations for obj1), picks best
- Single: Tries 100,000 moves (all objects × all destinations), picks best
Greedy vs Fast vs Full
| Move Type | Evaluation Strategy | Typical Moves Evaluated | Quality |
|---|---|---|---|
| SingleGreedy | Stop at first improvement | 1-100 | Lowest |
| SingleFast | Try all destinations for first improving object | 100-1K | Medium |
| Single | Try all objects and destinations | 10K-1M | Highest |
Complexity
Best case: O(1) - First move tried improves objective
Average case: O(N × C / k) where k = improvement frequency
Worst case: O(N × C) - No improving moves found
Where:
- N = number of objects in hot container
- C = number of containers
- k = average number of moves tried before finding improvement
Example - Interactive system:
- Hot container: 1,000 objects
- System: 100 containers
- Typical run: Finds improvement in 10-100 moves (vs 100K for Single)
Usage Patterns
Real-Time/Interactive Systems
Ultra-fast response for dashboards and UIs:
- Python
- C++
# Ultra-fast solver for UI/dashboard (100ms limit)
solver.addSolver(
SolverSpecs(
localSearchSolverSpec=LocalSearchSolverSpec(
solveTime=0.1, # 100ms limit for interactive response
moveTypeList=[
SingleGreedyMoveTypeSpec(), # First improvement = instant response
],
)
)
)
void interactive() {
// Ultra-fast solver for UI/dashboard (100ms limit)
auto executor = std::make_shared<folly::CPUThreadPoolExecutor>(4);
ProblemSolver solver(executor, "example", "test");
solver.setObjectName("task");
solver.setContainerName("host");
// Setup problem
solver.setAssignment(
std::map<std::string, std::vector<std::string>>{
{"host0", {"task0", "task1", "task2", "task3"}},
{"host1", {}},
{"host2", {}},
});
solver.addObjectDimension(
"cpu",
std::map<std::string, double>{
{"task0", 2.0},
{"task1", 1.5},
{"task2", 2.5},
{"task3", 1.0},
});
BalanceSpec balanceSpec;
balanceSpec.name() = "balance-cpu";
balanceSpec.scope() = "host";
balanceSpec.dimension() = "cpu";
solver.addGoal(balanceSpec, 1.0);
LocalSearchSolverSpec solverSpec;
solverSpec.solveTime() = 0.1; // 100ms limit for interactive response
solverSpec.moveTypeList()->emplace_back();
solverSpec.moveTypeList()->back().singleGreedyMoveTypeSpec() =
SingleGreedyMoveTypeSpec{};
solver.addSolver(solverSpec);
// Solve and print results
auto solution = solver.solve();
std::cout << " Initial objective: " << *solution.initialObjective()->value()
<< "\n";
std::cout << " Final objective: " << *solution.finalObjective()->value()
<< "\n";
std::cout << " Improvement: "
<< (*solution.initialObjective()->value() -
*solution.finalObjective()->value())
<< "\n";
}
Continuous Rebalancing
Frequent small adjustments:
- Python
- C++
# Run every 10 seconds for continuous rebalancing
# Each run makes 1-2 quick improvements
solver.addSolver(
SolverSpecs(
localSearchSolverSpec=LocalSearchSolverSpec(
solveTime=1, # 1 second per run
moveTypeList=[
SingleGreedyMoveTypeSpec(), # Fast incremental improvements
],
)
)
)
void continuous() {
// Run every 10 seconds for continuous rebalancing
// Each run makes 1-2 quick improvements
auto executor = std::make_shared<folly::CPUThreadPoolExecutor>(4);
ProblemSolver solver(executor, "example", "test");
LocalSearchSolverSpec solverSpec;
solverSpec.solveTime() = 1; // 1 second per run
solverSpec.moveTypeList()->emplace_back();
solverSpec.moveTypeList()->back().singleGreedyMoveTypeSpec() =
SingleGreedyMoveTypeSpec{};
solver.addSolver(solverSpec);
// Setup problem
solver.setAssignment(
std::map<std::string, std::vector<std::string>>{
{"host0", {"task0", "task1"}},
{"host1", {}},
});
solver.addObjectDimension(
"cpu",
std::map<std::string, double>{
{"task0", 1.0},
{"task1", 1.0},
});
BalanceSpec balanceSpec;
balanceSpec.name() = "balance-cpu";
balanceSpec.scope() = "host";
balanceSpec.dimension() = "cpu";
solver.addGoal(std::move(balanceSpec), 1.0);
// Solve and print results
auto solution = solver.solve();
std::cout << " Initial objective: " << *solution.initialObjective()->value()
<< "\n";
std::cout << " Final objective: " << *solution.finalObjective()->value()
<< "\n";
std::cout << " Improvement: "
<< (*solution.initialObjective()->value() -
*solution.finalObjective()->value())
<< "\n";
}
Combined with Slower Moves
Use greedy for quick wins, then thorough search:
- Python
- C++
# Multi-stage: greedy for quick wins, then thorough search
solver.addSolver(
SolverSpecs(
localSearchSolverSpec=LocalSearchSolverSpec(
moveTypeList=[
SingleGreedyMoveTypeSpec(), # Quick improvements first
SingleMoveTypeSpec(), # Then thorough exploration
SwapMoveTypeSpec(), # Finally handle capacity constraints
]
)
)
)
void combined() {
// Multi-stage: greedy for quick wins, then thorough search
auto executor = std::make_shared<folly::CPUThreadPoolExecutor>(4);
ProblemSolver solver(executor, "example", "test");
LocalSearchSolverSpec solverSpec;
// Add SingleGreedy
solverSpec.moveTypeList()->emplace_back();
solverSpec.moveTypeList()->back().singleGreedyMoveTypeSpec() =
SingleGreedyMoveTypeSpec{};
// Add Single
solverSpec.moveTypeList()->emplace_back();
solverSpec.moveTypeList()->back().singleMoveTypeSpec() = SingleMoveTypeSpec{};
// Add Swap
solverSpec.moveTypeList()->emplace_back();
solverSpec.moveTypeList()->back().swapMoveTypeSpec() = SwapMoveTypeSpec{};
solver.addSolver(solverSpec);
// Setup problem
solver.setAssignment(
std::map<std::string, std::vector<std::string>>{
{"host0", {"task0", "task1", "task2"}},
{"host1", {}},
{"host2", {}},
});
solver.addObjectDimension(
"cpu",
std::map<std::string, double>{
{"task0", 2.0},
{"task1", 1.5},
{"task2", 1.5},
});
BalanceSpec balanceSpec;
balanceSpec.name() = "balance-cpu";
balanceSpec.scope() = "host";
balanceSpec.dimension() = "cpu";
solver.addGoal(std::move(balanceSpec), 1.0);
// Solve and print results
auto solution = solver.solve();
std::cout << " Initial objective: " << *solution.initialObjective()->value()
<< "\n";
std::cout << " Final objective: " << *solution.finalObjective()->value()
<< "\n";
std::cout << " Improvement: "
<< (*solution.initialObjective()->value() -
*solution.finalObjective()->value())
<< "\n";
}
Quick Warm-Start
Fast initial solution before refinement:
- Python
- C++
# Stage 1: Fast warm-start with greedy
solver.addSolver(
SolverSpecs(
localSearchSolverSpec=LocalSearchSolverSpec(
solveTime=5, # Quick 5 second warm-start
moveTypeList=[
SingleGreedyMoveTypeSpec(), # Get to "good enough" quickly
],
)
)
)
# Stage 2: Refine with thorough search
solver.addSolver(
SolverSpecs(
localSearchSolverSpec=LocalSearchSolverSpec(
solveTime=60, # Spend more time refining
moveTypeList=[
SingleMoveTypeSpec(), # Find best moves
SwapMoveTypeSpec(), # Handle capacity constraints
],
)
)
)
void warmstart() {
// Fast initial solution before refinement
auto executor = std::make_shared<folly::CPUThreadPoolExecutor>(4);
ProblemSolver solver(executor, "example", "test");
// Stage 1: Fast warm-start with greedy
LocalSearchSolverSpec stage1;
stage1.solveTime() = 5; // Quick 5 second warm-start
stage1.moveTypeList()->emplace_back();
stage1.moveTypeList()->back().singleGreedyMoveTypeSpec() =
SingleGreedyMoveTypeSpec{};
solver.addSolver(stage1);
// Stage 2: Refine with thorough search
LocalSearchSolverSpec stage2;
stage2.solveTime() = 60; // Spend more time refining
stage2.moveTypeList()->emplace_back();
stage2.moveTypeList()->back().singleMoveTypeSpec() = SingleMoveTypeSpec{};
stage2.moveTypeList()->emplace_back();
stage2.moveTypeList()->back().swapMoveTypeSpec() = SwapMoveTypeSpec{};
solver.addSolver(stage2);
// Setup problem
solver.setAssignment(
std::map<std::string, std::vector<std::string>>{
{"host0", {"task0", "task1", "task2", "task3", "task4"}},
{"host1", {}},
{"host2", {}},
});
solver.addObjectDimension(
"cpu",
std::map<std::string, double>{
{"task0", 2.0},
{"task1", 1.5},
{"task2", 2.5},
{"task3", 1.0},
{"task4", 2.0},
});
BalanceSpec balanceSpec;
balanceSpec.name() = "balance-cpu";
balanceSpec.scope() = "host";
balanceSpec.dimension() = "cpu";
solver.addGoal(std::move(balanceSpec), 1.0);
// Solve and print results
auto solution = solver.solve();
std::cout << " Initial objective: " << *solution.initialObjective()->value()
<< "\n";
std::cout << " Final objective: " << *solution.finalObjective()->value()
<< "\n";
std::cout << " Improvement: "
<< (*solution.initialObjective()->value() -
*solution.finalObjective()->value())
<< "\n";
}
Performance Characteristics
Speed vs Quality Trade-off
| Metric | SingleGreedy | SingleFast | Single |
|---|---|---|---|
| Speed | Fastest | Fast | Slow |
| Moves evaluated | 1-100 | 100-1K | 10K-1M |
| Solution quality | 60-80% optimal | 80-95% optimal | 95-100% optimal |
| Iteration time | <1ms | 1-10ms | 10-1000ms |
| Convergence | Fast (many iterations) | Medium | Slow (few iterations) |
When Does It Help?
SingleGreedy helps when:
- Frequent updates: Problem changes often, re-runs are common
- Good initial state: Already near optimal, just need tweaks
- Interactive use: Human waiting for response
- Many small improvements: Easy to find some improvement quickly
SingleGreedy does NOT help when:
- Poor initial state: Need big improvements, not first improvement
- Rare improvements: Takes long to find ANY improvement
- Quality critical: Can't accept mediocre solutions
- One-shot optimization: Single run needs to be as good as possible
Comparison with Variants
| Move Type | Early Exit Strategy | Exploration Level | Use Case |
|---|---|---|---|
| Single | None | Full (all objects × all destinations) | Quality critical |
| SingleFast | First improving object | One object fully | Balanced |
| SingleGreedy | First improving move | Minimal | Speed critical |
| SingleRandomBatches | Batch-based | Random samples | Very large problems |
Decision tree:
- Need best solution? → Single
- Balanced quality/speed? → SingleFast
- Need fastest response? → SingleGreedy
- Problem huge (100K+ objects)? → SingleRandomBatches
Troubleshooting
Problem: Making many small moves, but objective not improving much
Diagnosis: Greedy approach finding local improvements but missing better global moves
Solutions:
- Switch to SingleFast or Single for better moves
- Use SingleGreedy for quick wins, then switch to thorough search
- Combine with other move types (Swap, Chain)
- Increase iteration limit to let greedy make more sequential moves
Problem: Not finding any improving moves
Diagnosis: Already at local optimum OR improvements are rare
Solutions:
- This is expected at local optimum - switch to more powerful move types
- Try Swap or SingleEndChain
- Check if constraints are too restrictive
- Verify objective function is correct
Problem: Solution quality much worse than expected
Diagnosis: Greedy strategy too aggressive, missing much better moves
Solutions:
- Use SingleFast for better quality (small speed cost)
- Use Single for best quality (larger speed cost)
- Run greedy multiple times with different random seeds
- Use greedy for warm-start, then refine with Single
Problem: Still too slow for interactive use
Diagnosis: Even single move evaluation is expensive
Solutions:
- Check objective function efficiency (is evaluation O(1)?)
- Reduce problem size (fewer objects/containers)
- Set strict
solveTimelimit (e.g., 100ms) - Pre-filter candidate destinations
- Consider if full rebalancing is needed for every change
When to Use SingleGreedy
DO use when:
- Interactive systems (dashboards, UIs)
- Continuous rebalancing (frequent small changes)
- Speed critical (<10ms response time needed)
- Initial solution is already decent
- Running frequently (can iterate to good solution over time)
DO NOT use when:
- One-shot optimization (single run needs to be optimal)
- Solution quality is critical
- Making infrequent large-scale changes
- Need deterministic/reproducible results
- Poor initial assignment (need big improvements)
Related Move Types
Similar speed optimization:
- SingleFast - Slightly slower, better quality
- SingleRandomBatches - For very large problems
Better quality alternatives:
Complementary:
- Use SingleGreedy first for quick wins
- Follow with Single/Swap for refinement
- Combine in multi-stage solver
Source Code
- Thrift definition:
interface/thrift/SolverSpecs.thrift:518 - Implementation:
solver/moves/SingleGreedyMoveType.h - Tests:
solver/moves/tests/
Next Steps
- Try SingleFast if SingleGreedy too aggressive
- Learn about Single for full exploration
- Review Performance Guide for speed optimization
- See Move Types Overview for choosing move types